@contember/engine-server 0.12.0-beta.2 → 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.
- package/alpine.dockerfile +2 -2
- package/debian.dockerfile +1 -1
- package/dist/src/MasterContainer.d.ts +4 -1
- package/dist/src/MasterContainer.d.ts.map +1 -1
- package/dist/src/MasterContainer.js +60 -31
- package/dist/src/MasterContainer.js.map +1 -1
- package/dist/src/bootstrap/Initializer.d.ts +4 -6
- package/dist/src/bootstrap/Initializer.d.ts.map +1 -1
- package/dist/src/bootstrap/Initializer.js +10 -11
- package/dist/src/bootstrap/Initializer.js.map +1 -1
- package/dist/src/config/config.d.ts +43 -22
- package/dist/src/config/config.d.ts.map +1 -1
- package/dist/src/config/config.js +135 -241
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/http/ShowMetricsMiddleware.js +1 -1
- package/dist/src/http/ShowMetricsMiddleware.js.map +1 -1
- package/dist/src/http/index.d.ts +0 -1
- package/dist/src/http/index.d.ts.map +1 -1
- package/dist/src/http/index.js +0 -1
- package/dist/src/http/index.js.map +1 -1
- package/dist/src/project/ProjectContainer.d.ts +1 -1
- package/dist/src/project/ProjectContainer.d.ts.map +1 -1
- package/dist/src/project/ProjectContainerResolver.d.ts +13 -10
- package/dist/src/project/ProjectContainerResolver.d.ts.map +1 -1
- package/dist/src/project/ProjectContainerResolver.js +48 -33
- package/dist/src/project/ProjectContainerResolver.js.map +1 -1
- package/dist/src/project/ProjectGroupState.d.ts +18 -0
- package/dist/src/project/ProjectGroupState.d.ts.map +1 -0
- package/dist/src/project/ProjectGroupState.js +30 -0
- package/dist/src/project/ProjectGroupState.js.map +1 -0
- package/dist/src/project/ProjectInitializer.d.ts +4 -10
- package/dist/src/project/ProjectInitializer.d.ts.map +1 -1
- package/dist/src/project/ProjectInitializer.js +5 -19
- package/dist/src/project/ProjectInitializer.js.map +1 -1
- package/dist/src/project/ProjectSchemaResolver.d.ts +5 -5
- package/dist/src/project/ProjectSchemaResolver.d.ts.map +1 -1
- package/dist/src/project/ProjectSchemaResolver.js +4 -4
- package/dist/src/project/ProjectSchemaResolver.js.map +1 -1
- package/dist/src/start.js +22 -14
- package/dist/src/start.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/utils/cluster.js +1 -1
- package/dist/src/utils/cluster.js.map +1 -1
- package/dist/tests/cases/http.js +12 -11
- package/dist/tests/cases/http.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/MasterContainer.ts +106 -55
- package/src/bootstrap/Initializer.ts +10 -17
- package/src/config/config.ts +159 -289
- package/src/http/index.ts +0 -1
- package/src/project/ProjectContainerResolver.ts +58 -35
- package/src/project/ProjectGroupState.ts +33 -0
- package/src/project/ProjectInitializer.ts +9 -23
- package/src/project/ProjectSchemaResolver.ts +12 -7
- package/src/start.ts +13 -6
- package/tests/cases/http.ts +7 -2
- package/dist/src/http/RootMiddleware.d.ts +0 -9
- package/dist/src/http/RootMiddleware.d.ts.map +0 -1
- package/dist/src/http/RootMiddleware.js +0 -33
- package/dist/src/http/RootMiddleware.js.map +0 -1
- package/src/http/RootMiddleware.ts +0 -53
package/src/config/config.ts
CHANGED
|
@@ -5,244 +5,167 @@ import {
|
|
|
5
5
|
resolveParameters,
|
|
6
6
|
UndefinedParameterError,
|
|
7
7
|
} from '@contember/config-loader'
|
|
8
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
port: number
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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 =
|
|
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: {
|
|
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
|
-
|
|
389
|
-
(
|
|
390
|
-
|
|
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,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
|
|
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
|
-
|
|
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 =
|
|
41
|
+
const existing = groupState.getContainer(slug)
|
|
35
42
|
if (existing) {
|
|
36
|
-
const
|
|
43
|
+
const existingAwaited = await existing
|
|
44
|
+
const state = await this.projectManager.getProjectState(projectGroup.database, slug, existingAwaited.timestamp)
|
|
37
45
|
if (state === 'valid') {
|
|
38
|
-
return
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|