@contember/engine-tenant-api 1.4.0-rc.2 → 1.4.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/src/TenantContainer.d.ts +3 -0
  2. package/dist/src/TenantContainer.d.ts.map +1 -1
  3. package/dist/src/TenantContainer.js +2 -0
  4. package/dist/src/TenantContainer.js.map +1 -1
  5. package/dist/src/migrations/2024-06-20-140000-mail-reply.d.ts +3 -0
  6. package/dist/src/migrations/2024-06-20-140000-mail-reply.d.ts.map +1 -0
  7. package/dist/src/migrations/2024-06-20-140000-mail-reply.js +11 -0
  8. package/dist/src/migrations/2024-06-20-140000-mail-reply.js.map +1 -0
  9. package/dist/src/migrations/runner.d.ts.map +1 -1
  10. package/dist/src/migrations/runner.js +2 -0
  11. package/dist/src/migrations/runner.js.map +1 -1
  12. package/dist/src/migrations/snapshot.d.ts.map +1 -1
  13. package/dist/src/migrations/snapshot.js +2 -1
  14. package/dist/src/migrations/snapshot.js.map +1 -1
  15. package/dist/src/model/authorization/PermissionActions.d.ts +5 -0
  16. package/dist/src/model/authorization/PermissionActions.d.ts.map +1 -1
  17. package/dist/src/model/authorization/PermissionActions.js +1 -0
  18. package/dist/src/model/authorization/PermissionActions.js.map +1 -1
  19. package/dist/src/model/authorization/PermissionsFactory.d.ts.map +1 -1
  20. package/dist/src/model/authorization/PermissionsFactory.js +1 -0
  21. package/dist/src/model/authorization/PermissionsFactory.js.map +1 -1
  22. package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.d.ts.map +1 -1
  23. package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.js +3 -2
  24. package/dist/src/model/commands/mailTemplate/AddMailTemplateCommand.js.map +1 -1
  25. package/dist/src/model/commands/mailTemplate/RemoveMailTemplateCommand.js +1 -2
  26. package/dist/src/model/commands/mailTemplate/RemoveMailTemplateCommand.js.map +1 -1
  27. package/dist/src/model/mailing/UserMailer.d.ts +1 -1
  28. package/dist/src/model/mailing/UserMailer.d.ts.map +1 -1
  29. package/dist/src/model/mailing/UserMailer.js +8 -2
  30. package/dist/src/model/mailing/UserMailer.js.map +1 -1
  31. package/dist/src/model/mailing/type.d.ts +5 -1
  32. package/dist/src/model/mailing/type.d.ts.map +1 -1
  33. package/dist/src/model/mailing/type.js +17 -1
  34. package/dist/src/model/mailing/type.js.map +1 -1
  35. package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.d.ts +9 -0
  36. package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.d.ts.map +1 -0
  37. package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.js +15 -0
  38. package/dist/src/model/queries/mailTemplate/MailTemplateListQuery.js.map +1 -0
  39. package/dist/src/model/queries/mailTemplate/MailTemplateQuery.d.ts +4 -10
  40. package/dist/src/model/queries/mailTemplate/MailTemplateQuery.d.ts.map +1 -1
  41. package/dist/src/model/queries/mailTemplate/MailTemplateQuery.js +3 -8
  42. package/dist/src/model/queries/mailTemplate/MailTemplateQuery.js.map +1 -1
  43. package/dist/src/model/queries/mailTemplate/helpers.d.ts +7 -0
  44. package/dist/src/model/queries/mailTemplate/helpers.d.ts.map +1 -0
  45. package/dist/src/model/queries/mailTemplate/helpers.js +16 -0
  46. package/dist/src/model/queries/mailTemplate/helpers.js.map +1 -0
  47. package/dist/src/model/queries/mailTemplate/index.d.ts +1 -0
  48. package/dist/src/model/queries/mailTemplate/index.d.ts.map +1 -1
  49. package/dist/src/model/queries/mailTemplate/index.js +1 -0
  50. package/dist/src/model/queries/mailTemplate/index.js.map +1 -1
  51. package/dist/src/model/service/PasswordResetManager.js +2 -1
  52. package/dist/src/model/service/PasswordResetManager.js.map +1 -1
  53. package/dist/src/resolvers/ResolverFactory.d.ts +3 -1
  54. package/dist/src/resolvers/ResolverFactory.d.ts.map +1 -1
  55. package/dist/src/resolvers/ResolverFactory.js +1 -0
  56. package/dist/src/resolvers/ResolverFactory.js.map +1 -1
  57. package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.d.ts +1 -2
  58. package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.d.ts.map +1 -1
  59. package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.js +12 -12
  60. package/dist/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.js.map +1 -1
  61. package/dist/src/resolvers/query/MailTemplateQueryResolver.d.ts +9 -0
  62. package/dist/src/resolvers/query/MailTemplateQueryResolver.d.ts.map +1 -0
  63. package/dist/src/resolvers/query/MailTemplateQueryResolver.js +22 -0
  64. package/dist/src/resolvers/query/MailTemplateQueryResolver.js.map +1 -0
  65. package/dist/src/schema/index.d.ts +27 -1
  66. package/dist/src/schema/index.d.ts.map +1 -1
  67. package/dist/src/schema/tenant.graphql.d.ts.map +1 -1
  68. package/dist/src/schema/tenant.graphql.js +14 -1
  69. package/dist/src/schema/tenant.graphql.js.map +1 -1
  70. package/dist/src/tsconfig.tsbuildinfo +1 -1
  71. package/dist/tests/cases/integration/mocked/sql/getMailTemplateSql.js +2 -2
  72. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +12 -12
  74. package/src/TenantContainer.ts +3 -0
  75. package/src/migrations/2024-06-20-140000-mail-reply.ts +10 -0
  76. package/src/migrations/runner.ts +2 -0
  77. package/src/migrations/snapshot.ts +2 -1
  78. package/src/model/authorization/PermissionActions.ts +1 -0
  79. package/src/model/authorization/PermissionsFactory.ts +1 -0
  80. package/src/model/commands/mailTemplate/AddMailTemplateCommand.ts +3 -1
  81. package/src/model/commands/mailTemplate/RemoveMailTemplateCommand.ts +1 -1
  82. package/src/model/mailing/UserMailer.ts +11 -5
  83. package/src/model/mailing/type.ts +20 -1
  84. package/src/model/queries/mailTemplate/MailTemplateListQuery.ts +16 -0
  85. package/src/model/queries/mailTemplate/MailTemplateQuery.ts +7 -20
  86. package/src/model/queries/mailTemplate/helpers.ts +20 -0
  87. package/src/model/queries/mailTemplate/index.ts +1 -0
  88. package/src/model/service/PasswordResetManager.ts +1 -1
  89. package/src/resolvers/ResolverFactory.ts +11 -5
  90. package/src/resolvers/mutation/mailTemplates/MailTemplateMutationResolver.ts +11 -17
  91. package/src/resolvers/query/MailTemplateQueryResolver.ts +23 -0
  92. package/src/schema/index.ts +29 -0
  93. package/src/schema/tenant.graphql.ts +14 -1
  94. package/tests/cases/integration/mocked/sql/getMailTemplateSql.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/engine-tenant-api",
3
- "version": "1.4.0-rc.2",
3
+ "version": "1.4.0-rc.3",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/src/index.js",
6
6
  "typings": "dist/src/index.d.ts",
@@ -13,16 +13,16 @@
13
13
  "test": "vitest --dir ./tests/cases --no-threads"
14
14
  },
15
15
  "dependencies": {
16
- "@contember/authorization": "1.4.0-rc.2",
17
- "@contember/config-loader": "1.4.0-rc.2",
18
- "@contember/database": "1.4.0-rc.2",
19
- "@contember/database-migrations": "1.4.0-rc.2",
20
- "@contember/dic": "1.4.0-rc.2",
21
- "@contember/graphql-utils": "1.4.0-rc.2",
22
- "@contember/logger": "1.4.0-rc.2",
23
- "@contember/schema": "1.4.0-rc.2",
24
- "@contember/schema-utils": "1.4.0-rc.2",
25
- "@contember/typesafe": "1.4.0-rc.2",
16
+ "@contember/authorization": "1.4.0-rc.3",
17
+ "@contember/config-loader": "1.4.0-rc.3",
18
+ "@contember/database": "1.4.0-rc.3",
19
+ "@contember/database-migrations": "1.4.0-rc.3",
20
+ "@contember/dic": "1.4.0-rc.3",
21
+ "@contember/graphql-utils": "1.4.0-rc.3",
22
+ "@contember/logger": "1.4.0-rc.3",
23
+ "@contember/schema": "1.4.0-rc.3",
24
+ "@contember/schema-utils": "1.4.0-rc.3",
25
+ "@contember/typesafe": "1.4.0-rc.3",
26
26
  "@graphql-tools/utils": "^10.0.8",
27
27
  "graphql-tag": "^2.12.6",
28
28
  "jose": "^5.1.1",
@@ -32,7 +32,7 @@
32
32
  "otpauth": "^9.1.5"
33
33
  },
34
34
  "devDependencies": {
35
- "@contember/database-tester": "1.4.0-rc.2",
35
+ "@contember/database-tester": "1.4.0-rc.3",
36
36
  "@graphql-codegen/cli": "^5.0.0",
37
37
  "@graphql-codegen/typescript": "^4.0.1",
38
38
  "@graphql-codegen/typescript-operations": "^4.0.1",
@@ -78,6 +78,7 @@ import { IDPQueryResolver } from './resolvers/query/IDPQueryResolver'
78
78
  import { UpdateIDPMutationResolver } from './resolvers/mutation/idp/UpdateIDPMutationResolver'
79
79
  import { TenantCredentials, TenantMigrationsRunner } from './migrations'
80
80
  import { DisablePersonMutationResolver } from './resolvers/mutation/person/DisablePersonMutationResolver'
81
+ import { MailTemplateQueryResolver } from './resolvers/query/MailTemplateQueryResolver'
81
82
 
82
83
  export interface TenantContainer {
83
84
  projectMemberManager: ProjectMemberManager
@@ -217,6 +218,8 @@ export class TenantContainerFactory {
217
218
  new ProjectQueryResolver(projectManager))
218
219
  .addService('projectMembersQueryResolver', ({ projectManager, projectMemberManager }) =>
219
220
  new ProjectMembersQueryResolver(projectManager, projectMemberManager))
221
+ .addService('mailTemplateQueryResolver', ({ mailTemplateManager }) =>
222
+ new MailTemplateQueryResolver(mailTemplateManager))
220
223
  .addService('signUpMutationResolver', ({ signUpManager, apiKeyManager }) =>
221
224
  new SignUpMutationResolver(signUpManager, apiKeyManager))
222
225
  .addService('signInMutationResolver', ({ signInManager, signInResponseFactory }) =>
@@ -0,0 +1,10 @@
1
+ import { MigrationBuilder } from '@contember/database-migrations'
2
+
3
+ const sql = `
4
+ ALTER TABLE mail_template
5
+ ADD COLUMN reply_to TEXT;
6
+ `
7
+
8
+ export default async function (builder: MigrationBuilder) {
9
+ builder.sql(sql)
10
+ }
@@ -36,6 +36,7 @@ import _20221108projectupdatenotification from './2022-11-08-project-update-noti
36
36
  import _20230120135500personemailnormalize from './2023-01-20-135500-person-email-normalize'
37
37
  import _20230603104000persondisabled from './2023-06-03-104000-person-disabled'
38
38
  import _20230901165000idpreturnconfig from './2023-09-01-165000-idp-return-config'
39
+ import _20240620140000mailreply from './2024-06-20-140000-mail-reply'
39
40
 
40
41
  import snapshot from './snapshot'
41
42
  import { computeTokenHash, Providers } from '../model'
@@ -85,6 +86,7 @@ const migrations = {
85
86
  '2023-01-20-135500-person-email-normalize': _20230120135500personemailnormalize,
86
87
  '2023-06-03-104000-person-disabled': _20230603104000persondisabled,
87
88
  '2023-09-01-165000-idp-return-config': _20230901165000idpreturnconfig,
89
+ '2024-06-20-140000-mail-reply': _20240620140000mailreply,
88
90
  }
89
91
 
90
92
  export class TenantMigrationsRunner {
@@ -64,7 +64,8 @@ CREATE TABLE "mail_template" (
64
64
  "variant" "text" NOT NULL,
65
65
  "subject" "text" NOT NULL,
66
66
  "content" "text" NOT NULL,
67
- "use_layout" boolean NOT NULL
67
+ "use_layout" boolean NOT NULL,
68
+ "reply_to" "text"
68
69
  );
69
70
  CREATE TABLE "person" (
70
71
  "id" "uuid" NOT NULL,
@@ -54,6 +54,7 @@ namespace PermissionActions {
54
54
 
55
55
  export const MAIL_TEMPLATE_ADD = Authorizator.createAction(Resources.mailTemplate, 'add')
56
56
  export const MAIL_TEMPLATE_REMOVE = Authorizator.createAction(Resources.mailTemplate, 'remove')
57
+ export const MAIL_TEMPLATE_LIST = Authorizator.createAction(Resources.mailTemplate, 'list')
57
58
 
58
59
  export const IDP_ADD = Authorizator.createAction(Resources.idp, 'add')
59
60
  export const IDP_UPDATE = Authorizator.createAction(Resources.idp, 'update')
@@ -54,6 +54,7 @@ class PermissionsFactory {
54
54
 
55
55
  permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_ADD)
56
56
  permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_REMOVE)
57
+ permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.MAIL_TEMPLATE_LIST)
57
58
 
58
59
  permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.IDP_ADD)
59
60
  permissions.allow(TenantRole.PROJECT_ADMIN, PermissionActions.IDP_UPDATE)
@@ -10,12 +10,13 @@ export class AddMailTemplateCommand implements Command<void> {
10
10
  .into('mail_template')
11
11
  .values({
12
12
  id: providers.uuid(),
13
- project_id: this.mailTemplate.projectId ?? null,
13
+ project_id: this.mailTemplate.projectId,
14
14
  mail_type: this.mailTemplate.type,
15
15
  variant: this.mailTemplate.variant,
16
16
  subject: this.mailTemplate.subject,
17
17
  content: this.mailTemplate.content,
18
18
  use_layout: this.mailTemplate.useLayout,
19
+ reply_to: this.mailTemplate.replyTo,
19
20
  })
20
21
  .onConflict(ConflictActionType.update, this.mailTemplate.projectId ? {
21
22
  columns: ['project_id', 'mail_type', 'variant'],
@@ -24,6 +25,7 @@ export class AddMailTemplateCommand implements Command<void> {
24
25
  columns: ['mail_type', 'variant'],
25
26
  where: it => it.isNull('project_id'),
26
27
  }, {
28
+ reply_to: expr => expr.select(['excluded', 'reply_to']),
27
29
  subject: expr => expr.select(['excluded', 'subject']),
28
30
  content: expr => expr.select(['excluded', 'content']),
29
31
  use_layout: expr => expr.select(['excluded', 'use_layout']),
@@ -10,7 +10,7 @@ export class RemoveMailTemplateCommand implements Command<boolean> {
10
10
  (await DeleteBuilder.create()
11
11
  .from('mail_template')
12
12
  .where({
13
- project_id: this.mailTemplateIdentifier.projectId ?? null,
13
+ project_id: this.mailTemplateIdentifier.projectId,
14
14
  mail_type: this.mailTemplateIdentifier.type,
15
15
  variant: this.mailTemplateIdentifier.variant,
16
16
  })
@@ -21,6 +21,7 @@ export class UserMailer {
21
21
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.newUserInvited, ...customMailOptions })) || {
22
22
  subject: 'You have been invited to {{project}}',
23
23
  content: NewUserInvited,
24
+ replyTo: null,
24
25
  }
25
26
  await this.sendTemplate(template, mailArguments)
26
27
  }
@@ -33,6 +34,7 @@ export class UserMailer {
33
34
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.existingUserInvited, ...customMailOptions })) || {
34
35
  subject: 'You have been invited to {{project}}',
35
36
  content: ExistingUserInvited,
37
+ replyTo: null,
36
38
  }
37
39
  await this.sendTemplate(template, mailArguments)
38
40
  }
@@ -40,17 +42,18 @@ export class UserMailer {
40
42
  async sendPasswordResetEmail(
41
43
  dbContext: DatabaseContext,
42
44
  mailArguments: { email: string; token: string; project?: string; projectSlug?: string },
43
- customMailOptions: { projectId?: string; variant: string },
45
+ customMailOptions: { projectId: string | null; variant: string },
44
46
  ): Promise<void> {
45
47
  const template = (await this.getCustomTemplate(dbContext, { type: MailType.passwordReset, ...customMailOptions })) || {
46
48
  subject: 'Password reset',
47
49
  content: PasswordReset,
50
+ replyTo: null,
48
51
  }
49
52
  await this.sendTemplate(template, mailArguments)
50
53
  }
51
54
 
52
55
  private async sendTemplate(
53
- template: Pick<MailTemplateData, 'subject' | 'content'>,
56
+ template: Pick<MailTemplateData, 'subject' | 'content' | 'replyTo'>,
54
57
  mailArguments: Record<string, any>,
55
58
  ) {
56
59
  const html = await this.templateRenderer.render(template.content, mailArguments)
@@ -58,21 +61,24 @@ export class UserMailer {
58
61
  to: mailArguments.email,
59
62
  subject: await this.templateRenderer.render(template.subject, mailArguments),
60
63
  html,
64
+ ...(template.replyTo ? {
65
+ replyTo: template.replyTo,
66
+ } : {}),
61
67
  })
62
68
  }
63
69
 
64
70
  private async getCustomTemplate(
65
71
  dbContext: DatabaseContext,
66
72
  identifier: MailTemplateIdentifier,
67
- ): Promise<Pick<MailTemplateData, 'subject' | 'content'> | null> {
73
+ ): Promise<Pick<MailTemplateData, 'subject' | 'content' | 'replyTo'> | null> {
68
74
  const customTemplate =
69
75
  (await dbContext.queryHandler.fetch(new MailTemplateQuery(identifier)))
70
- ?? (await dbContext.queryHandler.fetch(new MailTemplateQuery({ ...identifier, projectId: undefined })))
76
+ ?? (await dbContext.queryHandler.fetch(new MailTemplateQuery({ ...identifier, projectId: null })))
71
77
 
72
78
  if (!customTemplate) {
73
79
  return null
74
80
  }
75
81
  const content = customTemplate.useLayout ? Layout(customTemplate.content) : customTemplate.content
76
- return { content, subject: customTemplate.subject }
82
+ return { content, subject: customTemplate.subject, replyTo: customTemplate.replyTo }
77
83
  }
78
84
  }
@@ -1,11 +1,29 @@
1
+ import { MailType as SchemaMailType } from '../../schema'
2
+
1
3
  export enum MailType {
2
4
  existingUserInvited = 'existingUserInvited',
3
5
  newUserInvited = 'newUserInvited',
4
6
  passwordReset = 'passwordReset',
5
7
  }
6
8
 
9
+ export const mailTypeFromSchemaToDb = (type: SchemaMailType): MailType => {
10
+ return {
11
+ EXISTING_USER_INVITED: MailType.existingUserInvited,
12
+ NEW_USER_INVITED: MailType.newUserInvited,
13
+ RESET_PASSWORD_REQUEST: MailType.passwordReset,
14
+ }[type]
15
+ }
16
+
17
+ export const mailTypeFromDbToSchema = (type: MailType): SchemaMailType => {
18
+ return {
19
+ [MailType.existingUserInvited]: 'EXISTING_USER_INVITED' as const,
20
+ [MailType.newUserInvited]: 'NEW_USER_INVITED' as const,
21
+ [MailType.passwordReset]: 'RESET_PASSWORD_REQUEST' as const,
22
+ }[type]
23
+ }
24
+
7
25
  export interface MailTemplateIdentifier {
8
- projectId?: string
26
+ projectId: string | null
9
27
  type: MailType
10
28
  variant: string
11
29
  }
@@ -14,6 +32,7 @@ export interface MailTemplateData {
14
32
  subject: string
15
33
  content: string
16
34
  useLayout: boolean
35
+ replyTo: string | null
17
36
  }
18
37
 
19
38
  export interface MailTemplate extends MailTemplateIdentifier, MailTemplateData {}
@@ -0,0 +1,16 @@
1
+ import { DatabaseQuery, DatabaseQueryable } from '@contember/database'
2
+ import { createMailTemplateQuery, MailTemplateRow } from './helpers'
3
+
4
+ export type MailTemplateListRow =
5
+ & MailTemplateRow
6
+ & {
7
+ projectSlug: string | null
8
+ }
9
+ export class MailTemplateListQuery extends DatabaseQuery<MailTemplateListRow[]> {
10
+ async fetch({ db }: DatabaseQueryable): Promise<MailTemplateListRow[]> {
11
+ return await createMailTemplateQuery<MailTemplateListRow>()
12
+ .leftJoin('project', undefined, expr => expr.columnsEq(['project', 'id'], ['mail_template', 'project_id']))
13
+ .select(['project', 'slug'], 'projectSlug')
14
+ .getResult(db)
15
+ }
16
+ }
@@ -1,22 +1,18 @@
1
- import { DatabaseQuery, DatabaseQueryable, SelectBuilder } from '@contember/database'
2
- import { MailTemplateData, MailTemplateIdentifier } from '../../mailing'
1
+ import { DatabaseQuery, DatabaseQueryable } from '@contember/database'
2
+ import { MailTemplateIdentifier } from '../../mailing'
3
+ import { createMailTemplateQuery, MailTemplateRow } from './helpers'
3
4
 
4
- class MailTemplateQuery extends DatabaseQuery<MailTemplateQuery.Result> {
5
+ export class MailTemplateQuery extends DatabaseQuery<MailTemplateRow | null> {
5
6
  constructor(
6
7
  private mailTemplateIdentifier: MailTemplateIdentifier,
7
8
  ) {
8
9
  super()
9
10
  }
10
11
 
11
- async fetch({ db }: DatabaseQueryable): Promise<MailTemplateQuery.Result> {
12
- const rows = await SelectBuilder.create<MailTemplateQuery.Row>()
13
- .select('id')
14
- .select('subject')
15
- .select('content')
16
- .select('use_layout', 'useLayout')
17
- .from('mail_template')
12
+ async fetch({ db }: DatabaseQueryable): Promise<MailTemplateRow | null> {
13
+ const rows = await createMailTemplateQuery()
18
14
  .where({
19
- project_id: this.mailTemplateIdentifier.projectId ?? null,
15
+ project_id: this.mailTemplateIdentifier.projectId,
20
16
  mail_type: this.mailTemplateIdentifier.type,
21
17
  variant: this.mailTemplateIdentifier.variant,
22
18
  })
@@ -25,12 +21,3 @@ class MailTemplateQuery extends DatabaseQuery<MailTemplateQuery.Result> {
25
21
  return this.fetchOneOrNull(rows)
26
22
  }
27
23
  }
28
-
29
- namespace MailTemplateQuery {
30
- export type Row = {
31
- readonly id: string
32
- } & MailTemplateData
33
- export type Result = null | Row
34
- }
35
-
36
- export { MailTemplateQuery }
@@ -0,0 +1,20 @@
1
+ import { SelectBuilder } from '@contember/database'
2
+ import { MailTemplateData, MailTemplateIdentifier } from '../../mailing'
3
+
4
+ export type MailTemplateRow =
5
+ & {
6
+ readonly id: string
7
+ }
8
+ & MailTemplateData
9
+ & MailTemplateIdentifier
10
+
11
+ export const createMailTemplateQuery = <T extends MailTemplateRow>() => SelectBuilder.create<T>()
12
+ .select(['mail_template', 'id'])
13
+ .select('subject')
14
+ .select('content')
15
+ .select('use_layout', 'useLayout')
16
+ .select('reply_to', 'replyTo')
17
+ .select('project_id', 'projectId')
18
+ .select('mail_type', 'type')
19
+ .select('variant')
20
+ .from('mail_template')
@@ -1 +1,2 @@
1
1
  export * from './MailTemplateQuery'
2
+ export * from './MailTemplateListQuery'
@@ -49,7 +49,7 @@ export class PasswordResetManager {
49
49
  },
50
50
  {
51
51
  variant: mailOptions.mailVariant || '',
52
- projectId: project?.id,
52
+ projectId: project?.id ?? null,
53
53
  },
54
54
  )
55
55
  }
@@ -1,14 +1,18 @@
1
1
  import {
2
+ AddIDPMutationResolver,
2
3
  AddProjectMemberMutationResolver,
3
- ChangeProfileMutationResolver,
4
4
  ChangePasswordMutationResolver,
5
+ ChangeProfileMutationResolver,
5
6
  CreateApiKeyMutationResolver,
6
7
  CreateProjectMutationResolver,
7
- DisableApiKeyMutationResolver, DisableIDPMutationResolver, EnableIDPMutationResolver,
8
+ DisableApiKeyMutationResolver,
9
+ DisableIDPMutationResolver,
10
+ EnableIDPMutationResolver,
11
+ IdentityGlobalRolesMutationResolver,
8
12
  IDPMutationResolver,
9
13
  InviteMutationResolver,
10
14
  MailTemplateMutationResolver,
11
- OtpMutationResolver, AddIDPMutationResolver,
15
+ OtpMutationResolver,
12
16
  RemoveProjectMemberMutationResolver,
13
17
  ResetPasswordMutationResolver,
14
18
  SetProjectSecretMutationResolver,
@@ -16,7 +20,7 @@ import {
16
20
  SignOutMutationResolver,
17
21
  SignUpMutationResolver,
18
22
  UpdateProjectMemberMutationResolver,
19
- UpdateProjectMutationResolver, IdentityGlobalRolesMutationResolver,
23
+ UpdateProjectMutationResolver,
20
24
  } from './mutation'
21
25
 
22
26
  import { Resolvers } from '../schema'
@@ -26,7 +30,7 @@ import { DateTimeType, JSONType } from '@contember/graphql-utils'
26
30
  import { IDPQueryResolver } from './query/IDPQueryResolver'
27
31
  import { UpdateIDPMutationResolver } from './mutation/idp/UpdateIDPMutationResolver'
28
32
  import { DisablePersonMutationResolver } from './mutation/person/DisablePersonMutationResolver'
29
- import { } from './mutation/person/ChangeProfileMutationResolver'
33
+ import { MailTemplateQueryResolver } from './query/MailTemplateQueryResolver'
30
34
 
31
35
  class ResolverFactory {
32
36
  public constructor(
@@ -36,6 +40,7 @@ class ResolverFactory {
36
40
  projectQueryResolver: ProjectQueryResolver
37
41
  projectMembersQueryResolver: ProjectMembersQueryResolver
38
42
  idpQueryResolver: IDPQueryResolver
43
+ mailTemplateQueryResolver: MailTemplateQueryResolver
39
44
 
40
45
  signUpMutationResolver: SignUpMutationResolver
41
46
  signInMutationResolver: SignInMutationResolver
@@ -96,6 +101,7 @@ class ResolverFactory {
96
101
  projects: this.resolvers.projectQueryResolver.projects.bind(this.resolvers.projectQueryResolver),
97
102
  projectMemberships: this.resolvers.projectMembersQueryResolver.projectMemberships.bind(this.resolvers.projectMembersQueryResolver),
98
103
  identityProviders: this.resolvers.idpQueryResolver.identityProviders.bind(this.resolvers.idpQueryResolver),
104
+ mailTemplates: this.resolvers.mailTemplateQueryResolver.mailTemplates.bind(this.resolvers.mailTemplateQueryResolver),
99
105
  checkResetPasswordToken: () => {
100
106
  throw new Error('not implemented')
101
107
  },
@@ -1,16 +1,14 @@
1
1
  import {
2
- AddMailTemplateErrorCode,
3
2
  AddMailTemplateResponse,
4
- MailType as SchemaMailType,
5
3
  MutationAddProjectMailTemplateArgs,
6
4
  MutationRemoveProjectMailTemplateArgs,
7
5
  MutationResolvers,
8
- RemoveMailTemplateErrorCode,
9
6
  RemoveMailTemplateResponse,
10
7
  } from '../../../schema'
11
8
  import { TenantResolverContext } from '../../TenantResolverContext'
12
- import { MailTemplateManager, MailType, PermissionActions, ProjectManager } from '../../../model'
9
+ import { MailTemplateManager, mailTypeFromSchemaToDb, PermissionActions, ProjectManager } from '../../../model'
13
10
  import { createErrorResponse, createProjectNotFoundResponse } from '../../errorUtils'
11
+ import { validateEmail } from '../../../model/utils/email'
14
12
 
15
13
  export class MailTemplateMutationResolver implements MutationResolvers {
16
14
  constructor(
@@ -20,7 +18,7 @@ export class MailTemplateMutationResolver implements MutationResolvers {
20
18
 
21
19
  async addMailTemplate(
22
20
  parent: any,
23
- { template: { content, projectSlug, subject, type, useLayout, variant } }: MutationAddProjectMailTemplateArgs,
21
+ { template: { content, projectSlug, subject, type, useLayout, variant, replyTo } }: MutationAddProjectMailTemplateArgs,
24
22
  context: TenantResolverContext,
25
23
  ): Promise<AddMailTemplateResponse> {
26
24
  const project = projectSlug ? await this.projectManager.getProjectBySlug(context.db, projectSlug) : null
@@ -32,14 +30,18 @@ export class MailTemplateMutationResolver implements MutationResolvers {
32
30
  if (projectSlug && !project) {
33
31
  return createProjectNotFoundResponse('PROJECT_NOT_FOUND', projectSlug)
34
32
  }
33
+ if (replyTo && !validateEmail(replyTo)) {
34
+ return createErrorResponse('INVALID_REPLY_EMAIL_FORMAT', 'Reply-to email address is not in a valid format')
35
+ }
35
36
 
36
37
  await this.mailTemplateManager.addMailTemplate(context.db, {
37
38
  content,
38
- projectId: project?.id,
39
+ projectId: project?.id ?? null,
39
40
  subject,
40
41
  useLayout: typeof useLayout === 'boolean' ? useLayout : true,
41
42
  variant: variant || '',
42
- type: this.mapMailType(type),
43
+ type: mailTypeFromSchemaToDb(type),
44
+ replyTo: replyTo?.trim() || null,
43
45
  })
44
46
 
45
47
  return {
@@ -64,9 +66,9 @@ export class MailTemplateMutationResolver implements MutationResolvers {
64
66
  }
65
67
 
66
68
  const removed = await this.mailTemplateManager.removeMailTemplate(context.db, {
67
- projectId: project?.id,
69
+ projectId: project?.id ?? null,
68
70
  variant: variant || '',
69
- type: this.mapMailType(type),
71
+ type: mailTypeFromSchemaToDb(type),
70
72
  })
71
73
  if (!removed) {
72
74
  return createErrorResponse('PROJECT_NOT_FOUND', 'Mail template not found')
@@ -77,12 +79,4 @@ export class MailTemplateMutationResolver implements MutationResolvers {
77
79
  errors: [],
78
80
  }
79
81
  }
80
-
81
- private mapMailType(type: SchemaMailType): MailType {
82
- return {
83
- EXISTING_USER_INVITED: MailType.existingUserInvited,
84
- NEW_USER_INVITED: MailType.newUserInvited,
85
- RESET_PASSWORD_REQUEST: MailType.passwordReset,
86
- }[type]
87
- }
88
82
  }
@@ -0,0 +1,23 @@
1
+ import { MailTemplateData, QueryResolvers } from '../../schema'
2
+ import { MailTemplateListQuery, MailTemplateManager, mailTypeFromDbToSchema, PermissionActions } from '../../model'
3
+ import { TenantResolverContext } from '../TenantResolverContext'
4
+
5
+ export class MailTemplateQueryResolver implements QueryResolvers {
6
+
7
+ constructor(
8
+ private readonly mailTemplateManager: MailTemplateManager,
9
+ ) {
10
+ }
11
+
12
+ async mailTemplates(parent: unknown, args: unknown, context: TenantResolverContext): Promise<MailTemplateData[]> {
13
+ await context.requireAccess({
14
+ action: PermissionActions.IDP_LIST,
15
+ message: 'You are not allowed to list IDP',
16
+ })
17
+ return (await context.db.queryHandler.fetch(new MailTemplateListQuery())).map(it => ({
18
+ ...it,
19
+ variant: it.variant || null,
20
+ type: mailTypeFromDbToSchema(it.type),
21
+ }))
22
+ }
23
+ }
@@ -66,6 +66,7 @@ export type AddMailTemplateError = {
66
66
  }
67
67
 
68
68
  export type AddMailTemplateErrorCode =
69
+ | 'INVALID_REPLY_EMAIL_FORMAT'
69
70
  | 'MISSING_VARIABLE'
70
71
  | 'PROJECT_NOT_FOUND'
71
72
 
@@ -517,6 +518,7 @@ export type InviteResult = {
517
518
  export type MailTemplate = {
518
519
  readonly content: Scalars['String']['input']
519
520
  readonly projectSlug?: InputMaybe<Scalars['String']['input']>
521
+ readonly replyTo?: InputMaybe<Scalars['String']['input']>
520
522
  readonly subject: Scalars['String']['input']
521
523
  readonly type: MailType
522
524
  readonly useLayout?: InputMaybe<Scalars['Boolean']['input']>
@@ -524,6 +526,17 @@ export type MailTemplate = {
524
526
  readonly variant?: InputMaybe<Scalars['String']['input']>
525
527
  }
526
528
 
529
+ export type MailTemplateData = {
530
+ readonly __typename?: 'MailTemplateData'
531
+ readonly content: Scalars['String']['output']
532
+ readonly projectSlug?: Maybe<Scalars['String']['output']>
533
+ readonly replyTo?: Maybe<Scalars['String']['output']>
534
+ readonly subject: Scalars['String']['output']
535
+ readonly type: MailType
536
+ readonly useLayout: Scalars['Boolean']['output']
537
+ readonly variant?: Maybe<Scalars['String']['output']>
538
+ }
539
+
527
540
  export type MailTemplateIdentifier = {
528
541
  readonly projectSlug?: InputMaybe<Scalars['String']['input']>
529
542
  readonly type: MailType
@@ -904,6 +917,7 @@ export type Query = {
904
917
  readonly __typename?: 'Query'
905
918
  readonly checkResetPasswordToken: CheckResetPasswordTokenCode
906
919
  readonly identityProviders: ReadonlyArray<IdentityProvider>
920
+ readonly mailTemplates: ReadonlyArray<MailTemplateData>
907
921
  readonly me: Identity
908
922
  readonly personById?: Maybe<Person>
909
923
  readonly projectBySlug?: Maybe<Project>
@@ -1397,6 +1411,7 @@ export type ResolversTypes = {
1397
1411
  InviteResult: ResolverTypeWrapper<InviteResult>
1398
1412
  Json: ResolverTypeWrapper<Scalars['Json']['output']>
1399
1413
  MailTemplate: MailTemplate
1414
+ MailTemplateData: ResolverTypeWrapper<MailTemplateData>
1400
1415
  MailTemplateIdentifier: MailTemplateIdentifier
1401
1416
  MailType: MailType
1402
1417
  MemberType: MemberType
@@ -1530,6 +1545,7 @@ export type ResolversParentTypes = {
1530
1545
  InviteResult: InviteResult
1531
1546
  Json: Scalars['Json']['output']
1532
1547
  MailTemplate: MailTemplate
1548
+ MailTemplateData: MailTemplateData
1533
1549
  MailTemplateIdentifier: MailTemplateIdentifier
1534
1550
  Membership: Membership
1535
1551
  MembershipInput: MembershipInput
@@ -1945,6 +1961,17 @@ export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes
1945
1961
  name: 'Json'
1946
1962
  }
1947
1963
 
1964
+ export type MailTemplateDataResolvers<ContextType = any, ParentType extends ResolversParentTypes['MailTemplateData'] = ResolversParentTypes['MailTemplateData']> = {
1965
+ content?: Resolver<ResolversTypes['String'], ParentType, ContextType>
1966
+ projectSlug?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
1967
+ replyTo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
1968
+ subject?: Resolver<ResolversTypes['String'], ParentType, ContextType>
1969
+ type?: Resolver<ResolversTypes['MailType'], ParentType, ContextType>
1970
+ useLayout?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
1971
+ variant?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
1972
+ __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>
1973
+ }
1974
+
1948
1975
  export type MembershipResolvers<ContextType = any, ParentType extends ResolversParentTypes['Membership'] = ResolversParentTypes['Membership']> = {
1949
1976
  role?: Resolver<ResolversTypes['String'], ParentType, ContextType>
1950
1977
  variables?: Resolver<ReadonlyArray<ResolversTypes['VariableEntry']>, ParentType, ContextType>
@@ -2037,6 +2064,7 @@ export type ProjectIdentityRelationResolvers<ContextType = any, ParentType exten
2037
2064
  export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
2038
2065
  checkResetPasswordToken?: Resolver<ResolversTypes['CheckResetPasswordTokenCode'], ParentType, ContextType, RequireFields<QueryCheckResetPasswordTokenArgs, 'requestId' | 'token'>>
2039
2066
  identityProviders?: Resolver<ReadonlyArray<ResolversTypes['IdentityProvider']>, ParentType, ContextType>
2067
+ mailTemplates?: Resolver<ReadonlyArray<ResolversTypes['MailTemplateData']>, ParentType, ContextType>
2040
2068
  me?: Resolver<ResolversTypes['Identity'], ParentType, ContextType>
2041
2069
  personById?: Resolver<Maybe<ResolversTypes['Person']>, ParentType, ContextType, RequireFields<QueryPersonByIdArgs, 'id'>>
2042
2070
  projectBySlug?: Resolver<Maybe<ResolversTypes['Project']>, ParentType, ContextType, RequireFields<QueryProjectBySlugArgs, 'slug'>>
@@ -2321,6 +2349,7 @@ export type Resolvers<ContextType = any> = {
2321
2349
  InviteResponse?: InviteResponseResolvers<ContextType>
2322
2350
  InviteResult?: InviteResultResolvers<ContextType>
2323
2351
  Json?: GraphQLScalarType
2352
+ MailTemplateData?: MailTemplateDataResolvers<ContextType>
2324
2353
  Membership?: MembershipResolvers<ContextType>
2325
2354
  MembershipValidationError?: MembershipValidationErrorResolvers<ContextType>
2326
2355
  Mutation?: MutationResolvers<ContextType>
@@ -19,6 +19,7 @@ const schema: DocumentNode = gql`
19
19
  checkResetPasswordToken(requestId: String!, token: String!): CheckResetPasswordTokenCode!
20
20
 
21
21
  identityProviders: [IdentityProvider!]!
22
+ mailTemplates: [MailTemplateData!]!
22
23
  }
23
24
 
24
25
  type Mutation {
@@ -808,7 +809,17 @@ const schema: DocumentNode = gql`
808
809
  }
809
810
 
810
811
  # === mails ===
811
-
812
+
813
+ type MailTemplateData {
814
+ projectSlug: String
815
+ type: MailType!
816
+ variant: String
817
+ subject: String!
818
+ content: String!
819
+ useLayout: Boolean!
820
+ replyTo: String
821
+ }
822
+
812
823
  input MailTemplate {
813
824
  projectSlug: String
814
825
  type: MailType!
@@ -817,6 +828,7 @@ const schema: DocumentNode = gql`
817
828
  subject: String!
818
829
  content: String!
819
830
  useLayout: Boolean
831
+ replyTo: String
820
832
  }
821
833
 
822
834
  enum MailType {
@@ -846,6 +858,7 @@ const schema: DocumentNode = gql`
846
858
  enum AddMailTemplateErrorCode {
847
859
  MISSING_VARIABLE
848
860
  PROJECT_NOT_FOUND
861
+ INVALID_REPLY_EMAIL_FORMAT
849
862
  }
850
863
 
851
864
  type RemoveMailTemplateResponse {
@@ -3,7 +3,7 @@ import { ExpectedQuery } from '@contember/database-tester'
3
3
  export const getMailTemplateSql = (
4
4
  args: { type: string; projectId: string | null },
5
5
  ): ExpectedQuery => args.projectId ? {
6
- sql: `SELECT "id", "subject", "content", "use_layout" AS "uselayout"
6
+ sql: `select "mail_template"."id", "subject", "content", "use_layout" as "useLayout", "reply_to" as "replyTo", "project_id" as "projectId", "mail_type" as "type", "variant"
7
7
  FROM "tenant"."mail_template"
8
8
  WHERE "project_id" = ?
9
9
  AND "mail_type" = ?
@@ -14,7 +14,7 @@ export const getMailTemplateSql = (
14
14
  },
15
15
  } :
16
16
  {
17
- sql: `SELECT "id", "subject", "content", "use_layout" AS "uselayout"
17
+ sql: `select "mail_template"."id", "subject", "content", "use_layout" as "useLayout", "reply_to" as "replyTo", "project_id" as "projectId", "mail_type" as "type", "variant"
18
18
  FROM "tenant"."mail_template"
19
19
  WHERE "project_id" IS NULL
20
20
  AND "mail_type" = ?