@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.
- package/alpine.dockerfile +3 -1
- package/debian.dockerfile +3 -1
- package/dist/src/MasterContainer.d.ts +104 -8
- package/dist/src/MasterContainer.d.ts.map +1 -1
- package/dist/src/MasterContainer.js +65 -84
- package/dist/src/MasterContainer.js.map +1 -1
- package/dist/src/bootstrap/Initializer.d.ts +3 -12
- package/dist/src/bootstrap/Initializer.d.ts.map +1 -1
- package/dist/src/bootstrap/Initializer.js +8 -24
- package/dist/src/bootstrap/Initializer.js.map +1 -1
- package/dist/src/config/config.d.ts +7 -49
- package/dist/src/config/config.d.ts.map +1 -1
- package/dist/src/config/config.js +11 -188
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/configSchema.d.ts +84 -0
- package/dist/src/config/configSchema.d.ts.map +1 -0
- package/dist/src/config/configSchema.js +80 -0
- package/dist/src/config/configSchema.js.map +1 -0
- package/dist/src/config/configTemplate.d.ts +2 -0
- package/dist/src/config/configTemplate.d.ts.map +1 -0
- package/dist/src/config/configTemplate.js +62 -0
- package/dist/src/config/configTemplate.js.map +1 -0
- package/dist/src/config/projectConfigResolver.d.ts +8 -0
- package/dist/src/config/projectConfigResolver.d.ts.map +1 -0
- package/dist/src/config/projectConfigResolver.js +63 -0
- package/dist/src/config/projectConfigResolver.js.map +1 -0
- package/dist/src/config/tenantConfigResolver.d.ts +8 -0
- package/dist/src/config/tenantConfigResolver.d.ts.map +1 -0
- package/dist/src/config/tenantConfigResolver.js +31 -0
- package/dist/src/config/tenantConfigResolver.js.map +1 -0
- package/dist/src/http/CollectHttpMetricsMiddelware.d.ts +2 -2
- package/dist/src/http/CollectHttpMetricsMiddelware.d.ts.map +1 -1
- package/dist/src/http/CollectHttpMetricsMiddelware.js +6 -5
- package/dist/src/http/CollectHttpMetricsMiddelware.js.map +1 -1
- package/dist/src/project/ProjectContainer.d.ts +14 -16
- package/dist/src/project/ProjectContainer.d.ts.map +1 -1
- package/dist/src/project/ProjectContainer.js +17 -12
- package/dist/src/project/ProjectContainer.js.map +1 -1
- package/dist/src/project/ProjectContainerResolver.d.ts +12 -10
- package/dist/src/project/ProjectContainerResolver.d.ts.map +1 -1
- package/dist/src/project/ProjectContainerResolver.js +26 -33
- package/dist/src/project/ProjectContainerResolver.js.map +1 -1
- package/dist/src/project/{ProjectGroupState.d.ts → ProjectContainerStore.d.ts} +2 -2
- package/dist/src/project/ProjectContainerStore.d.ts.map +1 -0
- package/dist/src/project/{ProjectGroupState.js → ProjectContainerStore.js} +4 -4
- package/dist/src/project/ProjectContainerStore.js.map +1 -0
- package/dist/src/project/ProjectInitializer.d.ts +3 -3
- package/dist/src/project/ProjectInitializer.d.ts.map +1 -1
- package/dist/src/project/ProjectInitializer.js +4 -4
- package/dist/src/project/ProjectInitializer.js.map +1 -1
- package/dist/src/project/ProjectSchemaResolver.d.ts +3 -3
- package/dist/src/project/ProjectSchemaResolver.d.ts.map +1 -1
- package/dist/src/project/ProjectSchemaResolver.js +5 -5
- package/dist/src/project/ProjectSchemaResolver.js.map +1 -1
- package/dist/src/projectGroup/ProjectGroupContainer.d.ts +23 -0
- package/dist/src/projectGroup/ProjectGroupContainer.d.ts.map +1 -0
- package/dist/src/projectGroup/ProjectGroupContainer.js +64 -0
- package/dist/src/projectGroup/ProjectGroupContainer.js.map +1 -0
- package/dist/src/projectGroup/ProjectGroupContainerResolver.d.ts +12 -0
- package/dist/src/projectGroup/ProjectGroupContainerResolver.d.ts.map +1 -0
- package/dist/src/projectGroup/ProjectGroupContainerResolver.js +36 -0
- package/dist/src/projectGroup/ProjectGroupContainerResolver.js.map +1 -0
- package/dist/src/start.js +9 -8
- package/dist/src/start.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/utils/dbMetrics.d.ts +6 -2
- package/dist/src/utils/dbMetrics.d.ts.map +1 -1
- package/dist/src/utils/dbMetrics.js +13 -16
- package/dist/src/utils/dbMetrics.js.map +1 -1
- package/dist/tests/cases/http.js +30 -19
- package/dist/tests/cases/http.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -21
- package/src/MasterContainer.ts +145 -204
- package/src/bootstrap/Initializer.ts +9 -34
- package/src/config/config.ts +23 -234
- package/src/config/configSchema.ts +114 -0
- package/src/config/configTemplate.ts +58 -0
- package/src/config/projectConfigResolver.ts +83 -0
- package/src/config/tenantConfigResolver.ts +41 -0
- package/src/http/CollectHttpMetricsMiddelware.ts +6 -5
- package/src/project/ProjectContainer.ts +25 -18
- package/src/project/ProjectContainerResolver.ts +29 -37
- package/src/project/{ProjectGroupState.ts → ProjectContainerStore.ts} +1 -1
- package/src/project/ProjectInitializer.ts +5 -9
- package/src/project/ProjectSchemaResolver.ts +6 -10
- package/src/projectGroup/ProjectGroupContainer.ts +120 -0
- package/src/projectGroup/ProjectGroupContainerResolver.ts +47 -0
- package/src/start.ts +9 -8
- package/src/utils/dbMetrics.ts +19 -20
- package/tests/cases/http.ts +35 -22
- package/dist/src/project/ProjectGroupState.d.ts.map +0 -1
- package/dist/src/project/ProjectGroupState.js.map +0 -1
package/src/config/config.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
15
|
-
export type TenantConfig =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
36
|
+
configTemplate,
|
|
195
37
|
)
|
|
196
38
|
|
|
197
|
-
|
|
39
|
+
const rawConfig = Merger.merge(template, ...configs)
|
|
198
40
|
|
|
199
41
|
const parametersResolver = createObjectParametersResolver({ env })
|
|
200
|
-
config = resolveParameters(
|
|
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
|
|
44
|
+
const serverConfig = serverConfigSchema(config)
|
|
220
45
|
return {
|
|
221
|
-
|
|
222
|
-
projectConfigResolver: (
|
|
223
|
-
|
|
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,
|
|
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<
|
|
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
|
|
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
|
}
|