@contember/engine-server 1.1.0-alpha.2 → 1.1.0-alpha.5
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
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { Builder } from '@contember/dic'
|
|
2
2
|
import { Connection } from '@contember/database'
|
|
3
3
|
import { DatabaseContextFactory, SchemaVersionBuilder } from '@contember/engine-system-api'
|
|
4
|
-
import { logSentryError } from '../utils'
|
|
5
|
-
import { ModificationHandlerFactory } from '@contember/schema-migrations'
|
|
6
4
|
import { GraphQlSchemaBuilderFactory, PermissionsByIdentityFactory } from '@contember/engine-content-api'
|
|
7
5
|
import { GraphQLSchemaContributor, Plugin } from '@contember/engine-plugins'
|
|
8
6
|
import {
|
|
9
|
-
ProjectConfig,
|
|
10
|
-
ContentQueryHandlerProvider,
|
|
11
7
|
ContentSchemaResolver,
|
|
12
8
|
GraphQlSchemaFactory,
|
|
13
|
-
|
|
9
|
+
ProjectConfig,
|
|
14
10
|
ProjectContainer,
|
|
11
|
+
Providers,
|
|
15
12
|
} from '@contember/engine-http'
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
export class ProjectContainerFactoryFactory {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly debug: boolean,
|
|
17
|
+
private readonly plugins: Plugin[],
|
|
18
|
+
private readonly providers: Providers,
|
|
19
|
+
) {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
create(schemaVersionBuilder: SchemaVersionBuilder): ProjectContainerFactory {
|
|
23
|
+
return new ProjectContainerFactory(this.debug, this.plugins, schemaVersionBuilder, this.providers)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProjectContainerFactoryArgs {
|
|
28
|
+
project: ProjectConfig
|
|
29
|
+
}
|
|
17
30
|
|
|
18
31
|
export class ProjectContainerFactory {
|
|
19
32
|
constructor(
|
|
@@ -23,19 +36,19 @@ export class ProjectContainerFactory {
|
|
|
23
36
|
private readonly providers: Providers,
|
|
24
37
|
) {}
|
|
25
38
|
|
|
26
|
-
public createContainer(
|
|
27
|
-
return this.createBuilder(
|
|
39
|
+
public createContainer(args: ProjectContainerFactoryArgs): ProjectContainer {
|
|
40
|
+
return this.createBuilder(args)
|
|
28
41
|
.build()
|
|
29
42
|
.pick(
|
|
30
43
|
'project',
|
|
31
44
|
'connection',
|
|
32
|
-
'contentQueryHandlerProvider',
|
|
33
45
|
'systemDatabaseContextFactory',
|
|
34
46
|
'contentSchemaResolver',
|
|
47
|
+
'graphQlSchemaFactory',
|
|
35
48
|
)
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
protected createBuilder(project:
|
|
51
|
+
protected createBuilder({ project }: ProjectContainerFactoryArgs) {
|
|
39
52
|
return new Builder({})
|
|
40
53
|
.addService('providers', () =>
|
|
41
54
|
this.providers)
|
|
@@ -45,8 +58,6 @@ export class ProjectContainerFactory {
|
|
|
45
58
|
project)
|
|
46
59
|
.addService('connection', ({ project }) =>
|
|
47
60
|
new Connection(project.db, { timing: true }))
|
|
48
|
-
.addService('modificationHandlerFactory', () =>
|
|
49
|
-
new ModificationHandlerFactory(ModificationHandlerFactory.defaultFactoryMap))
|
|
50
61
|
.addService('graphQlSchemaBuilderFactory', () =>
|
|
51
62
|
new GraphQlSchemaBuilderFactory())
|
|
52
63
|
.addService('permissionsByIdentityFactory', ({}) =>
|
|
@@ -57,13 +68,9 @@ export class ProjectContainerFactory {
|
|
|
57
68
|
.filter((it): it is GraphQLSchemaContributor => !!it)
|
|
58
69
|
return new GraphQlSchemaFactory(graphQlSchemaBuilderFactory, permissionsByIdentityFactory, contributors)
|
|
59
70
|
})
|
|
60
|
-
.addService('contentQueryHandlerFactory', () =>
|
|
61
|
-
new ContentQueryHandlerFactory(project.slug, this.debug, logSentryError))
|
|
62
71
|
.addService('contentSchemaResolver', ({ schemaVersionBuilder }) =>
|
|
63
72
|
new ContentSchemaResolver(schemaVersionBuilder))
|
|
64
|
-
.addService('
|
|
65
|
-
new
|
|
66
|
-
.addService('systemDatabaseContextFactory', ({ connection, providers }) =>
|
|
67
|
-
new DatabaseContextFactory(connection.createClient('system', { module: 'system' }), providers))
|
|
73
|
+
.addService('systemDatabaseContextFactory', ({ connection, providers, project }) =>
|
|
74
|
+
new DatabaseContextFactory(connection.createClient(project.db.systemSchema ?? 'system', { module: 'system' }), providers))
|
|
68
75
|
}
|
|
69
76
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { ProjectContainerFactory } from './ProjectContainer'
|
|
2
|
-
import {
|
|
3
|
-
import { ProjectManager, ProjectWithSecrets } from '@contember/engine-tenant-api'
|
|
4
|
-
import { ProjectGroup } from '@contember/engine-tenant-api'
|
|
2
|
+
import { ProjectContainer } from '@contember/engine-http'
|
|
3
|
+
import { DatabaseContext, ProjectManager, ProjectWithSecrets } from '@contember/engine-tenant-api'
|
|
5
4
|
import { ProjectInitializer as SystemProjectInitializer } from '@contember/engine-system-api'
|
|
6
|
-
import {
|
|
5
|
+
import { ProjectContainerStore } from './ProjectContainerStore'
|
|
7
6
|
import { Logger } from '@contember/engine-common'
|
|
7
|
+
import { ProjectConfigResolver } from '../config/projectConfigResolver'
|
|
8
|
+
import { TenantConfig } from '../config/config'
|
|
8
9
|
|
|
9
10
|
export class ProjectContainerResolver {
|
|
10
|
-
private
|
|
11
|
+
private projectContainers = new ProjectContainerStore()
|
|
11
12
|
|
|
12
13
|
public readonly onCreate: ((container: ProjectContainer) => void | (() => void))[] = []
|
|
13
14
|
|
|
@@ -16,13 +17,15 @@ export class ProjectContainerResolver {
|
|
|
16
17
|
private readonly projectConfigResolver: ProjectConfigResolver,
|
|
17
18
|
private readonly projectManager: ProjectManager,
|
|
18
19
|
private readonly systemProjectInitializer: SystemProjectInitializer,
|
|
20
|
+
private readonly tenantDatabase: DatabaseContext,
|
|
21
|
+
private readonly tenantConfig: TenantConfig,
|
|
19
22
|
) {}
|
|
20
23
|
|
|
21
|
-
public async getAllProjectContainers(
|
|
22
|
-
const projects = await this.projectManager.getProjects(
|
|
24
|
+
public async getAllProjectContainers(): Promise<ProjectContainer[]> {
|
|
25
|
+
const projects = await this.projectManager.getProjects(this.tenantDatabase)
|
|
23
26
|
return await Promise.all(
|
|
24
27
|
projects.map(async it => {
|
|
25
|
-
const container = await this.getProjectContainer(
|
|
28
|
+
const container = await this.getProjectContainer(it.slug)
|
|
26
29
|
if (!container) {
|
|
27
30
|
throw new Error('should not happen')
|
|
28
31
|
}
|
|
@@ -31,41 +34,40 @@ export class ProjectContainerResolver {
|
|
|
31
34
|
)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
public async getProjectContainer(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const realSlug = groupState.resolveAlias(slug)
|
|
37
|
+
public async getProjectContainer(slug: string, alias: boolean = false): Promise<ProjectContainer | undefined> {
|
|
38
|
+
const realSlug = this.projectContainers.resolveAlias(slug)
|
|
38
39
|
if (realSlug) {
|
|
39
40
|
slug = realSlug
|
|
40
41
|
}
|
|
41
|
-
const existing =
|
|
42
|
+
const existing = this.projectContainers.getContainer(slug)
|
|
42
43
|
if (existing) {
|
|
43
44
|
const existingAwaited = await existing
|
|
44
|
-
const state = await this.projectManager.getProjectState(
|
|
45
|
+
const state = await this.projectManager.getProjectState(this.tenantDatabase, slug, existingAwaited.timestamp)
|
|
45
46
|
if (state === 'valid') {
|
|
46
47
|
return (await existingAwaited).container
|
|
47
48
|
}
|
|
48
|
-
await this.destroyContainer(
|
|
49
|
+
await this.destroyContainer(slug)
|
|
49
50
|
if (state === 'not_found') {
|
|
50
51
|
return undefined
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
const project = await this.projectManager.getProjectWithSecretsBySlug(
|
|
54
|
+
const project = await this.projectManager.getProjectWithSecretsBySlug(this.tenantDatabase, slug, alias)
|
|
54
55
|
if (!project) {
|
|
55
56
|
return undefined
|
|
56
57
|
}
|
|
57
|
-
const container = await this.createProjectContainer(
|
|
58
|
+
const container = await this.createProjectContainer(project)
|
|
58
59
|
if (slug !== project.slug) {
|
|
59
|
-
|
|
60
|
+
this.projectContainers.setAlias(project.slug, slug)
|
|
60
61
|
}
|
|
61
62
|
return container
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
public async createProjectContainer(
|
|
65
|
-
const projectConfig = this.projectConfigResolver(project.slug, project.config, project.secrets)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
public async createProjectContainer(project: ProjectWithSecrets): Promise<ProjectContainer> {
|
|
66
|
+
const projectConfig = this.projectConfigResolver(project.slug, project.config, project.secrets, this.tenantConfig)
|
|
67
|
+
return (await this.projectContainers.fetchContainer(project.slug, async slug => {
|
|
68
|
+
const projectContainer = this.projectContainerFactory.createContainer({
|
|
69
|
+
project: projectConfig,
|
|
70
|
+
})
|
|
69
71
|
await this.systemProjectInitializer.initialize(
|
|
70
72
|
projectContainer.systemDatabaseContextFactory,
|
|
71
73
|
projectContainer.project,
|
|
@@ -81,14 +83,13 @@ export class ProjectContainerResolver {
|
|
|
81
83
|
})).container
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
public async destroyContainer(
|
|
85
|
-
const
|
|
86
|
-
const existing = groupState.getContainer(slug)
|
|
86
|
+
public async destroyContainer(slug: string): Promise<void> {
|
|
87
|
+
const existing = this.projectContainers.getContainer(slug)
|
|
87
88
|
if (existing) {
|
|
88
89
|
const existingAwaited = await existing
|
|
89
90
|
existingAwaited.cleanups.forEach(it => it())
|
|
90
|
-
existingAwaited.container.project.alias?.forEach(it =>
|
|
91
|
-
|
|
91
|
+
existingAwaited.container.project.alias?.forEach(it => this.projectContainers.removeAlias(it))
|
|
92
|
+
this.projectContainers.removeContainer(slug)
|
|
92
93
|
}
|
|
93
94
|
process.nextTick(() => {
|
|
94
95
|
if (global.gc) {
|
|
@@ -96,13 +97,4 @@ export class ProjectContainerResolver {
|
|
|
96
97
|
}
|
|
97
98
|
})
|
|
98
99
|
}
|
|
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
|
-
}
|
|
108
100
|
}
|
|
@@ -3,7 +3,7 @@ import { PromiseMap } from '@contember/engine-common'
|
|
|
3
3
|
|
|
4
4
|
type ContainerWithMeta = { container: ProjectContainer; cleanups: (() => void)[]; timestamp: Date }
|
|
5
5
|
|
|
6
|
-
export class
|
|
6
|
+
export class ProjectContainerStore {
|
|
7
7
|
private containers = new PromiseMap<string, ContainerWithMeta>()
|
|
8
8
|
private aliasMapping = new Map<string, string>()
|
|
9
9
|
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ProjectGroup,
|
|
3
|
-
ProjectInitializer as ProjectInitializerInterface,
|
|
4
|
-
ProjectWithSecrets,
|
|
5
|
-
} from '@contember/engine-tenant-api'
|
|
1
|
+
import { ProjectInitializer as ProjectInitializerInterface, ProjectWithSecrets } from '@contember/engine-tenant-api'
|
|
6
2
|
import { ProjectContainerResolver } from './ProjectContainerResolver'
|
|
7
3
|
|
|
8
4
|
export class ProjectInitializer implements ProjectInitializerInterface {
|
|
@@ -10,8 +6,8 @@ export class ProjectInitializer implements ProjectInitializerInterface {
|
|
|
10
6
|
private readonly projectContainerResolver: ProjectContainerResolver,
|
|
11
7
|
) {}
|
|
12
8
|
|
|
13
|
-
async initializeProject(
|
|
14
|
-
await this.projectContainerResolver.createProjectContainer(
|
|
9
|
+
async initializeProject(project: ProjectWithSecrets) {
|
|
10
|
+
await this.projectContainerResolver.createProjectContainer(project)
|
|
15
11
|
}
|
|
16
12
|
}
|
|
17
13
|
|
|
@@ -22,10 +18,10 @@ export class ProjectInitializerProxy implements ProjectInitializerInterface {
|
|
|
22
18
|
this.initializer = initializer
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
initializeProject(
|
|
21
|
+
initializeProject(project: ProjectWithSecrets) {
|
|
26
22
|
if (!this.initializer) {
|
|
27
23
|
throw new Error('Initializer is not set')
|
|
28
24
|
}
|
|
29
|
-
return this.initializer.initializeProject(
|
|
25
|
+
return this.initializer.initializeProject(project)
|
|
30
26
|
}
|
|
31
27
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DatabaseContext,
|
|
3
|
-
ProjectGroup,
|
|
4
|
-
ProjectSchemaResolver as ProjectSchemaResolverInterface,
|
|
5
|
-
} from '@contember/engine-tenant-api'
|
|
1
|
+
import { ProjectSchemaResolver as ProjectSchemaResolverInterface } from '@contember/engine-tenant-api'
|
|
6
2
|
import { SchemaVersionBuilder } from '@contember/engine-system-api'
|
|
7
3
|
import { ProjectContainerResolver } from '@contember/engine-http'
|
|
8
4
|
import { Schema } from '@contember/schema'
|
|
@@ -13,12 +9,12 @@ export class ProjectSchemaResolver implements ProjectSchemaResolverInterface {
|
|
|
13
9
|
private readonly schemaVersionBuilder: SchemaVersionBuilder,
|
|
14
10
|
) {}
|
|
15
11
|
|
|
16
|
-
async getSchema(
|
|
17
|
-
const container = await this.projectContainerResolver.getProjectContainer(
|
|
12
|
+
async getSchema(slug: string) {
|
|
13
|
+
const container = await this.projectContainerResolver.getProjectContainer(slug)
|
|
18
14
|
if (!container) {
|
|
19
15
|
return undefined
|
|
20
16
|
}
|
|
21
|
-
const db = container.systemDatabaseContextFactory.create(
|
|
17
|
+
const db = container.systemDatabaseContextFactory.create()
|
|
22
18
|
return await this.schemaVersionBuilder.buildSchema(db)
|
|
23
19
|
}
|
|
24
20
|
}
|
|
@@ -30,10 +26,10 @@ export class ProjectSchemaResolverProxy implements ProjectSchemaResolverInterfac
|
|
|
30
26
|
this.resolver = resolver
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
getSchema(
|
|
29
|
+
getSchema(projectSlug: string): Promise<Schema | undefined> {
|
|
34
30
|
if (!this.resolver) {
|
|
35
31
|
throw new Error('Resolved is not set')
|
|
36
32
|
}
|
|
37
|
-
return this.resolver.getSchema(
|
|
33
|
+
return this.resolver.getSchema(projectSlug)
|
|
38
34
|
}
|
|
39
35
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Builder } from '@contember/dic'
|
|
2
|
+
import { Connection } from '@contember/database'
|
|
3
|
+
import { TenantContainerFactory } from '@contember/engine-tenant-api'
|
|
4
|
+
import { TenantConfig } from '../config/config'
|
|
5
|
+
import {
|
|
6
|
+
Authenticator,
|
|
7
|
+
CryptoWrapper,
|
|
8
|
+
ProjectGroupContainer,
|
|
9
|
+
Providers,
|
|
10
|
+
SystemGraphQLHandlerFactory,
|
|
11
|
+
TenantGraphQLHandlerFactory,
|
|
12
|
+
} from '@contember/engine-http'
|
|
13
|
+
import {
|
|
14
|
+
ProjectContainerFactoryFactory,
|
|
15
|
+
ProjectContainerResolver,
|
|
16
|
+
ProjectInitializer,
|
|
17
|
+
ProjectInitializerProxy,
|
|
18
|
+
ProjectSchemaResolver,
|
|
19
|
+
ProjectSchemaResolverProxy,
|
|
20
|
+
} from '../project'
|
|
21
|
+
import { SystemContainerFactory } from '@contember/engine-system-api'
|
|
22
|
+
import { ProjectConfigResolver } from '../config/projectConfigResolver'
|
|
23
|
+
import { createSecretKey } from 'crypto'
|
|
24
|
+
|
|
25
|
+
interface ProjectGroupContainerFactoryArgs
|
|
26
|
+
{
|
|
27
|
+
config: TenantConfig
|
|
28
|
+
slug: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ProjectGroupContainerFactory {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly providers: Providers,
|
|
34
|
+
private readonly systemContainerFactory: SystemContainerFactory,
|
|
35
|
+
private readonly tenantContainerFactory: TenantContainerFactory,
|
|
36
|
+
private readonly projectContainerFactoryFactory: ProjectContainerFactoryFactory,
|
|
37
|
+
private readonly projectConfigResolver: ProjectConfigResolver,
|
|
38
|
+
private readonly tenantGraphQLHandlerFactory: TenantGraphQLHandlerFactory,
|
|
39
|
+
private readonly systemGraphQLHandlerFactory: SystemGraphQLHandlerFactory,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
public create({ config, slug }: ProjectGroupContainerFactoryArgs): ProjectGroupContainer {
|
|
43
|
+
return new Builder({})
|
|
44
|
+
.addService('slug', () =>
|
|
45
|
+
slug)
|
|
46
|
+
.addService('providers', () =>
|
|
47
|
+
this.providers)
|
|
48
|
+
.addService('tenantConnection', (): Connection.ConnectionType =>
|
|
49
|
+
new Connection(config.db, {}))
|
|
50
|
+
.addService('projectSchemaResolver', () =>
|
|
51
|
+
new ProjectSchemaResolverProxy())
|
|
52
|
+
.addService('projectInitializer', () =>
|
|
53
|
+
new ProjectInitializerProxy())
|
|
54
|
+
.addService('tenantContainer', ({ tenantConnection, projectSchemaResolver, projectInitializer }) => {
|
|
55
|
+
const encryptionKey = config.secrets.encryptionKey
|
|
56
|
+
? createSecretKey(Buffer.from(config.secrets.encryptionKey, 'hex'))
|
|
57
|
+
: undefined
|
|
58
|
+
|
|
59
|
+
const cryptoWrapper = new CryptoWrapper(encryptionKey)
|
|
60
|
+
return this.tenantContainerFactory.create({
|
|
61
|
+
connection: tenantConnection,
|
|
62
|
+
mailOptions: config.mailer,
|
|
63
|
+
tenantCredentials: config.credentials,
|
|
64
|
+
projectInitializer,
|
|
65
|
+
projectSchemaResolver,
|
|
66
|
+
cryptoProviders: {
|
|
67
|
+
decrypt: cryptoWrapper.decrypt.bind(cryptoWrapper),
|
|
68
|
+
encrypt: cryptoWrapper.encrypt.bind(cryptoWrapper),
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
.addService('tenantDatabase', ({ tenantContainer }) =>
|
|
73
|
+
tenantContainer.databaseContext)
|
|
74
|
+
.addService('identityFetcher', ({ tenantContainer: { identityFetcher } }) =>
|
|
75
|
+
identityFetcher)
|
|
76
|
+
.addService('systemContainer', ({ identityFetcher }) =>
|
|
77
|
+
this.systemContainerFactory.create({ identityFetcher }))
|
|
78
|
+
.addService('schemaVersionBuilder', ({ systemContainer }) =>
|
|
79
|
+
systemContainer.schemaVersionBuilder)
|
|
80
|
+
.addService('projectContainerFactory', ({ schemaVersionBuilder }) =>
|
|
81
|
+
this.projectContainerFactoryFactory.create(schemaVersionBuilder))
|
|
82
|
+
.addService('tenantProjectManager', ({ tenantContainer }) =>
|
|
83
|
+
tenantContainer.projectManager)
|
|
84
|
+
.addService('projectContainerResolver', ({ projectContainerFactory, tenantProjectManager, systemContainer, tenantContainer }) =>
|
|
85
|
+
new ProjectContainerResolver(
|
|
86
|
+
projectContainerFactory,
|
|
87
|
+
this.projectConfigResolver,
|
|
88
|
+
tenantProjectManager,
|
|
89
|
+
systemContainer.projectInitializer,
|
|
90
|
+
tenantContainer.databaseContext,
|
|
91
|
+
config,
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
.setupService('projectSchemaResolver', (it, { projectContainerResolver, schemaVersionBuilder }) => {
|
|
95
|
+
it.setResolver(new ProjectSchemaResolver(projectContainerResolver, schemaVersionBuilder))
|
|
96
|
+
})
|
|
97
|
+
.setupService('projectInitializer', (it, { projectContainerResolver }) => {
|
|
98
|
+
it.setInitializer(new ProjectInitializer(projectContainerResolver))
|
|
99
|
+
})
|
|
100
|
+
.addService('tenantGraphQLHandler', ({ tenantContainer }) =>
|
|
101
|
+
this.tenantGraphQLHandlerFactory.create(tenantContainer.resolvers))
|
|
102
|
+
.addService('systemGraphQLHandler', ({ systemContainer }) =>
|
|
103
|
+
this.systemGraphQLHandlerFactory.create(systemContainer.systemResolversFactory))
|
|
104
|
+
.addService('authenticator', ({ tenantDatabase, tenantContainer }) =>
|
|
105
|
+
new Authenticator(tenantDatabase, tenantContainer.apiKeyManager))
|
|
106
|
+
.build()
|
|
107
|
+
.pick(
|
|
108
|
+
'projectContainerResolver',
|
|
109
|
+
'projectSchemaResolver',
|
|
110
|
+
'projectInitializer',
|
|
111
|
+
'systemContainer',
|
|
112
|
+
'systemGraphQLHandler',
|
|
113
|
+
'tenantContainer',
|
|
114
|
+
'tenantDatabase',
|
|
115
|
+
'authenticator',
|
|
116
|
+
'tenantGraphQLHandler',
|
|
117
|
+
'slug',
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TenantConfigResolver } from '../config/tenantConfigResolver'
|
|
2
|
+
import {
|
|
3
|
+
ProjectGroupContainer,
|
|
4
|
+
ProjectGroupContainerResolver as ProjectGroupContainerResolverInterface,
|
|
5
|
+
} from '@contember/engine-http'
|
|
6
|
+
import { ProjectGroupContainerFactory } from './ProjectGroupContainer'
|
|
7
|
+
import { PromiseMap } from '@contember/engine-common'
|
|
8
|
+
import { isDeepStrictEqual } from 'util'
|
|
9
|
+
|
|
10
|
+
interface ContainerWithMeta {
|
|
11
|
+
container: ProjectGroupContainer
|
|
12
|
+
cleanups: (() => void)[]
|
|
13
|
+
inputConfig: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ProjectGroupContainerResolver implements ProjectGroupContainerResolverInterface {
|
|
17
|
+
private containers = new PromiseMap<string | undefined, ContainerWithMeta>()
|
|
18
|
+
|
|
19
|
+
public readonly onCreate: ((container: ProjectGroupContainer, slug: string | undefined) => void | (() => void))[] = []
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly configResolver: TenantConfigResolver,
|
|
23
|
+
private readonly containerFactory: ProjectGroupContainerFactory,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
public async getProjectGroupContainer(slug: string | undefined, config: any = {}): Promise<ProjectGroupContainer> {
|
|
27
|
+
const existing = this.containers.get(slug)
|
|
28
|
+
if (existing) {
|
|
29
|
+
const existingAwaited = await existing
|
|
30
|
+
if (isDeepStrictEqual(config, existingAwaited.inputConfig)) {
|
|
31
|
+
return existingAwaited.container
|
|
32
|
+
}
|
|
33
|
+
existingAwaited.cleanups.forEach(it => it())
|
|
34
|
+
this.containers.delete(slug)
|
|
35
|
+
}
|
|
36
|
+
return (await this.containers.fetch(slug, async slug => {
|
|
37
|
+
const container = this.containerFactory.create({
|
|
38
|
+
config: this.configResolver(slug, config),
|
|
39
|
+
slug,
|
|
40
|
+
})
|
|
41
|
+
const cleanups = this.onCreate.map(it => it(container, slug) || (() => null))
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
await container.tenantContainer.migrationsRunner.run(console.log)
|
|
44
|
+
return { container, inputConfig: config, cleanups }
|
|
45
|
+
})).container
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/start.ts
CHANGED
|
@@ -68,13 +68,13 @@ const createServerTerminator = (): Server[] => {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const {
|
|
71
|
+
const { serverConfig, projectConfigResolver, tenantConfigResolver } = await readConfig(configSources, configProcessors)
|
|
72
72
|
|
|
73
73
|
if (process.argv[2] === 'validate') {
|
|
74
74
|
process.exit(0)
|
|
75
75
|
}
|
|
76
|
-
'sentry' in
|
|
77
|
-
const workerConfig =
|
|
76
|
+
'sentry' in serverConfig.logging && initSentry(serverConfig.logging.sentry?.dsn)
|
|
77
|
+
const workerConfig = serverConfig.workerCount || 1
|
|
78
78
|
|
|
79
79
|
const workerCount = workerConfig === 'auto' ? os.cpus().length : Number(workerConfig)
|
|
80
80
|
const isClusterMode = workerCount > 1
|
|
@@ -83,8 +83,9 @@ const createServerTerminator = (): Server[] => {
|
|
|
83
83
|
|
|
84
84
|
const container = createContainer({
|
|
85
85
|
debugMode: isDebug,
|
|
86
|
-
|
|
86
|
+
serverConfig,
|
|
87
87
|
projectConfigResolver,
|
|
88
|
+
tenantConfigResolver,
|
|
88
89
|
plugins,
|
|
89
90
|
processType,
|
|
90
91
|
version,
|
|
@@ -92,7 +93,7 @@ const createServerTerminator = (): Server[] => {
|
|
|
92
93
|
|
|
93
94
|
let initializedProjects: string[] = []
|
|
94
95
|
if (cluster.isMaster) {
|
|
95
|
-
const monitoringPort =
|
|
96
|
+
const monitoringPort = serverConfig.monitoringPort
|
|
96
97
|
const terminator = createServerTerminator()
|
|
97
98
|
terminator.push(
|
|
98
99
|
container.monitoringKoa.listen(monitoringPort, () => {
|
|
@@ -100,14 +101,14 @@ const createServerTerminator = (): Server[] => {
|
|
|
100
101
|
console.log(`Monitoring running on http://localhost:${monitoringPort}`)
|
|
101
102
|
}),
|
|
102
103
|
)
|
|
103
|
-
if (!
|
|
104
|
+
if (!serverConfig.projectGroup) {
|
|
104
105
|
initializedProjects = await container.initializer.initialize()
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
const port =
|
|
109
|
+
const port = serverConfig.port
|
|
109
110
|
const printStarted = () => {
|
|
110
|
-
if (
|
|
111
|
+
if (serverConfig.projectGroup) {
|
|
111
112
|
// eslint-disable-next-line no-console
|
|
112
113
|
console.log('Contember Cloud running')
|
|
113
114
|
} else {
|
package/src/utils/dbMetrics.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import prom from 'prom-client'
|
|
2
2
|
import { Connection, EventManager } from '@contember/database'
|
|
3
3
|
|
|
4
|
+
const labelNames = ['contember_project' as const, 'contember_module' as const, 'contember_project_group' as const]
|
|
5
|
+
type LabelNames = typeof labelNames[number]
|
|
6
|
+
type Labels = { [K in LabelNames]: string }
|
|
4
7
|
type DatabaseMetricsEntry = {
|
|
5
8
|
connection: Connection.PoolStatusProvider & Connection.Queryable
|
|
6
|
-
|
|
7
|
-
project: string
|
|
9
|
+
labels: Labels
|
|
8
10
|
}
|
|
9
11
|
type Unregistrar = () => void
|
|
10
12
|
type DatabaseMetricsRegistrar = (entry: DatabaseMetricsEntry) => Unregistrar
|
|
@@ -14,17 +16,14 @@ export const createDbMetricsRegistrar = (registry: prom.Registry): DatabaseMetri
|
|
|
14
16
|
const entries = new Set<DatabaseMetricsEntry>()
|
|
15
17
|
const collector = () => {
|
|
16
18
|
for (const entry of entries) {
|
|
17
|
-
dbPoolCollectorInner(entry.connection,
|
|
19
|
+
dbPoolCollectorInner(entry.connection, entry.labels)
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
registry.registerCollector(collector)
|
|
21
23
|
const sqlMetricsRegistrar = createSqlMetricsRegistrar(registry)
|
|
22
24
|
return entry => {
|
|
23
25
|
entries.add(entry)
|
|
24
|
-
const sqlMetricsUnregistrar = sqlMetricsRegistrar(entry.connection,
|
|
25
|
-
contember_module: entry.module,
|
|
26
|
-
contember_project: entry.project,
|
|
27
|
-
})
|
|
26
|
+
const sqlMetricsUnregistrar = sqlMetricsRegistrar(entry.connection, entry.labels)
|
|
28
27
|
return () => {
|
|
29
28
|
entries.delete(entry)
|
|
30
29
|
sqlMetricsUnregistrar()
|
|
@@ -33,33 +32,33 @@ export const createDbMetricsRegistrar = (registry: prom.Registry): DatabaseMetri
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const createSingleDbPoolMetricsCollector = (registry: prom.Registry) => {
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
const totalCount = new prom.Gauge({
|
|
38
37
|
registers: [registry],
|
|
39
38
|
name: `contember_db_pool_total_count`,
|
|
40
39
|
help: 'The total number of clients existing within the pool by contember_module (tenant or content; system is not used since it uses the same connection) and contember_project for contember_module=content.',
|
|
41
|
-
labelNames
|
|
40
|
+
labelNames,
|
|
42
41
|
})
|
|
43
42
|
const idleCount = new prom.Gauge({
|
|
44
43
|
registers: [registry],
|
|
45
44
|
name: `contember_db_pool_idle_count`,
|
|
46
45
|
help: 'The number of clients which are not checked out but are currently idle in the pool by contember_module (tenant or content; system is not used since it uses the same connection) and contember_project for contember_module=content.',
|
|
47
|
-
labelNames
|
|
46
|
+
labelNames,
|
|
48
47
|
})
|
|
49
48
|
const waitingCount = new prom.Gauge({
|
|
50
49
|
registers: [registry],
|
|
51
50
|
name: `contember_db_pool_waiting_count`,
|
|
52
51
|
help: 'The number of queued requests waiting on a client when all clients are checked out. It can be helpful to monitor this number to see if you need to adjust the size of the pool by contember_module (tenant or content; system is not used since it uses the same connection) and contember_project for contember_module=content.',
|
|
53
|
-
labelNames
|
|
52
|
+
labelNames,
|
|
54
53
|
})
|
|
55
54
|
const maxCount = new prom.Gauge({
|
|
56
55
|
registers: [registry],
|
|
57
56
|
name: `contember_db_pool_max_count`,
|
|
58
57
|
help: 'Maximum number of clients the pool should contain by contember_module (tenant or content; system is not used since it uses the same connection) and contember_project for contember_module=content.',
|
|
59
|
-
labelNames
|
|
58
|
+
labelNames,
|
|
60
59
|
})
|
|
61
60
|
|
|
62
|
-
return (db: Connection.PoolStatusProvider, labels:
|
|
61
|
+
return (db: Connection.PoolStatusProvider, labels: Labels) => {
|
|
63
62
|
const poolInfo = db.getPoolStatus()
|
|
64
63
|
totalCount.set(labels, poolInfo.totalCount)
|
|
65
64
|
idleCount.set(labels, poolInfo.idleCount)
|
|
@@ -69,20 +68,20 @@ const createSingleDbPoolMetricsCollector = (registry: prom.Registry) => {
|
|
|
69
68
|
}
|
|
70
69
|
type SqlMetricsRegistrar = (
|
|
71
70
|
connection: Connection.Queryable,
|
|
72
|
-
labels:
|
|
71
|
+
labels: Labels,
|
|
73
72
|
) => Unregistrar
|
|
74
73
|
const createSqlMetricsRegistrar = (registry: prom.Registry): SqlMetricsRegistrar => {
|
|
75
74
|
const sqlDuration = new prom.Histogram({
|
|
76
75
|
name: 'contember_sql_duration_ms',
|
|
77
76
|
help: 'Executed SQL queries by contember_module (system, tenant, content or unknown) and contember_project (or "unknown" for unknown project)',
|
|
78
77
|
registers: [registry],
|
|
79
|
-
labelNames
|
|
78
|
+
labelNames,
|
|
80
79
|
buckets: [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000],
|
|
81
80
|
})
|
|
82
81
|
const sqlErrorRate = new prom.Counter({
|
|
83
82
|
name: 'contember_sql_error_count',
|
|
84
83
|
help: 'Failed SQL queries by contember_module (system, tenant, content or unknown) and contember_project (or "unknown" for unknown project)',
|
|
85
|
-
labelNames
|
|
84
|
+
labelNames,
|
|
86
85
|
registers: [registry],
|
|
87
86
|
})
|
|
88
87
|
|
|
@@ -90,16 +89,16 @@ const createSqlMetricsRegistrar = (registry: prom.Registry): SqlMetricsRegistrar
|
|
|
90
89
|
const queryEndCallback: EventManager.ListenerTypes[EventManager.Event.queryEnd] = ({ meta }, { timing }) => {
|
|
91
90
|
sqlDuration.observe(
|
|
92
91
|
{
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
...labels,
|
|
93
|
+
contember_module: meta.module || labels.contember_module,
|
|
95
94
|
},
|
|
96
95
|
timing ? timing.selfDuration / 1000 : 0,
|
|
97
96
|
)
|
|
98
97
|
}
|
|
99
98
|
const queryErrorCallback: EventManager.ListenerTypes[EventManager.Event.queryError] = ({ meta }) => {
|
|
100
99
|
sqlErrorRate.inc({
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
...labels,
|
|
101
|
+
contember_module: meta.module || labels.contember_module,
|
|
103
102
|
})
|
|
104
103
|
}
|
|
105
104
|
connection.eventManager.on(EventManager.Event.queryEnd, queryEndCallback)
|