@technomoron/mail-magic 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.do-realease.sh +10 -0
  2. package/.editorconfig +9 -0
  3. package/.env-dist +62 -0
  4. package/.prettierrc +14 -0
  5. package/.vscode/extensions.json +15 -0
  6. package/.vscode/settings.json +123 -0
  7. package/CHANGES +25 -0
  8. package/README.md +63 -0
  9. package/config-example/form-template/default.njk +102 -0
  10. package/config-example/forms.config.json +8 -0
  11. package/config-example/init-data.json +33 -0
  12. package/config-example/tx-template/default.njk +107 -0
  13. package/dist/api/forms.js +175 -0
  14. package/dist/api/mailer.js +213 -0
  15. package/dist/index.js +50 -0
  16. package/dist/models/db.js +99 -0
  17. package/dist/models/domain.js +58 -0
  18. package/dist/models/form.js +168 -0
  19. package/dist/models/init.js +176 -0
  20. package/dist/models/txmail.js +167 -0
  21. package/dist/models/user.js +65 -0
  22. package/dist/server.js +22 -0
  23. package/dist/store/envloader.js +116 -0
  24. package/dist/store/store.js +85 -0
  25. package/dist/types.js +1 -0
  26. package/dist/util.js +94 -0
  27. package/ecosystem.config.cjs +42 -0
  28. package/eslint.config.mjs +104 -0
  29. package/package.json +67 -0
  30. package/src/api/forms.ts +209 -0
  31. package/src/api/mailer.ts +242 -0
  32. package/src/index.ts +67 -0
  33. package/src/models/db.ts +112 -0
  34. package/src/models/domain.ts +72 -0
  35. package/src/models/form.ts +198 -0
  36. package/src/models/init.ts +237 -0
  37. package/src/models/txmail.ts +199 -0
  38. package/src/models/user.ts +79 -0
  39. package/src/server.ts +27 -0
  40. package/src/store/envloader.ts +117 -0
  41. package/src/store/store.ts +116 -0
  42. package/src/types.ts +39 -0
  43. package/src/util.ts +111 -0
  44. package/test1.sh +13 -0
  45. package/tsconfig.json +14 -0
@@ -0,0 +1,209 @@
1
+ import path from 'path';
2
+
3
+ import { ApiRoute, ApiRequest, ApiModule, ApiError } from '@technomoron/api-server-base';
4
+ import nunjucks from 'nunjucks';
5
+
6
+ import { api_domain } from '../models/domain.js';
7
+ import { api_form } from '../models/form.js';
8
+ import { api_user } from '../models/user.js';
9
+ import { mailApiServer } from '../server.js';
10
+ import { buildRequestMeta, normalizeSlug } from '../util.js';
11
+
12
+ import type { mailApiRequest, UploadedFile } from '../types.js';
13
+
14
+ export class FormAPI extends ApiModule<mailApiServer> {
15
+ private async assertDomainAndUser(apireq: mailApiRequest): Promise<void> {
16
+ const { domain, locale } = apireq.req.body;
17
+
18
+ if (!domain) {
19
+ throw new ApiError({ code: 401, message: 'Missing domain' });
20
+ }
21
+ const user = await api_user.findOne({ where: { token: apireq.token } });
22
+ if (!user) {
23
+ throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
24
+ }
25
+ const dbdomain = await api_domain.findOne({ where: { name: domain } });
26
+ if (!dbdomain) {
27
+ throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
28
+ }
29
+ apireq.domain = dbdomain;
30
+ apireq.locale = locale || 'en';
31
+ apireq.user = user;
32
+ }
33
+
34
+ private async postFormTemplate(apireq: mailApiRequest): Promise<[number, { Status: string }]> {
35
+ await this.assertDomainAndUser(apireq);
36
+
37
+ const {
38
+ template,
39
+ sender = '',
40
+ recipient = '',
41
+ idname,
42
+ subject = '',
43
+ locale = '',
44
+ secret = ''
45
+ } = apireq.req.body;
46
+
47
+ if (!template) {
48
+ throw new ApiError({ code: 400, message: 'Missing template data' });
49
+ }
50
+ if (!idname) {
51
+ throw new ApiError({ code: 400, message: 'Missing form identifier' });
52
+ }
53
+ if (!sender) {
54
+ throw new ApiError({ code: 400, message: 'Missing sender address' });
55
+ }
56
+ if (!recipient) {
57
+ throw new ApiError({ code: 400, message: 'Missing recipient address' });
58
+ }
59
+
60
+ const user = apireq.user!;
61
+ const domain = apireq.domain!;
62
+ const resolvedLocale = locale || apireq.locale || '';
63
+ const userSlug = normalizeSlug(user.idname);
64
+ const domainSlug = normalizeSlug(domain.name);
65
+ const formSlug = normalizeSlug(idname);
66
+ const localeSlug = normalizeSlug(resolvedLocale || domain.locale || user.locale || '');
67
+ const slug = `${userSlug}-${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
68
+ const filenameParts = [userSlug, domainSlug, 'form-template'];
69
+ if (localeSlug) {
70
+ filenameParts.push(localeSlug);
71
+ }
72
+ filenameParts.push(formSlug);
73
+ let filename = path.join(...filenameParts);
74
+ if (!filename.endsWith('.njk')) {
75
+ filename += '.njk';
76
+ }
77
+
78
+ const record = {
79
+ user_id: user.user_id,
80
+ domain_id: domain.domain_id,
81
+ locale: localeSlug,
82
+ idname,
83
+ sender,
84
+ recipient,
85
+ subject,
86
+ template,
87
+ slug,
88
+ filename,
89
+ secret,
90
+ files: []
91
+ };
92
+
93
+ try {
94
+ const [form, created] = await api_form.upsert(record, {
95
+ returning: true,
96
+ conflictFields: ['user_id', 'domain_id', 'locale', 'idname']
97
+ });
98
+ this.server.storage.print_debug(`Form template upserted: ${form.idname} (created=${created})`);
99
+ } catch (error: unknown) {
100
+ throw new ApiError({
101
+ code: 500,
102
+ message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error on upsert form template')
103
+ });
104
+ }
105
+
106
+ return [200, { Status: 'OK' }];
107
+ }
108
+
109
+ private async postSendForm(apireq: ApiRequest): Promise<[number, Record<string, unknown>]> {
110
+ const { formid, secret, recipient, vars = {} } = apireq.req.body;
111
+
112
+ if (!formid) {
113
+ throw new ApiError({ code: 404, message: 'Missing formid field in form' });
114
+ }
115
+
116
+ const form = await api_form.findOne({ where: { idname: formid } });
117
+ if (!form) {
118
+ throw new ApiError({ code: 404, message: `No such form: ${formid}` });
119
+ }
120
+
121
+ if (form.secret && !secret) {
122
+ throw new ApiError({ code: 401, message: 'This form requires a secret key' });
123
+ }
124
+ if (form.secret && form.secret !== secret) {
125
+ throw new ApiError({ code: 401, message: 'Bad form secret' });
126
+ }
127
+ if (recipient && !form.secret) {
128
+ throw new ApiError({ code: 401, message: "'recipient' parameterer requires form secret to be set" });
129
+ }
130
+
131
+ let parsedVars: unknown = vars ?? {};
132
+ if (typeof vars === 'string') {
133
+ try {
134
+ parsedVars = JSON.parse(vars);
135
+ } catch (error) {
136
+ throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
137
+ }
138
+ }
139
+ const thevars = parsedVars as Record<string, unknown>;
140
+
141
+ /*
142
+ console.log('Headers:', apireq.req.headers);
143
+ console.log('Body:', JSON.stringify(apireq.req.body, null, 2));
144
+ console.log('Files:', JSON.stringify(apireq.req.files, null, 2));
145
+ */
146
+
147
+ const rawFiles = Array.isArray(apireq.req.files) ? (apireq.req.files as UploadedFile[]) : [];
148
+ const attachments = rawFiles.map((file) => ({
149
+ filename: file.originalname,
150
+ path: file.path
151
+ }));
152
+
153
+ const attachmentMap: Record<string, string> = {};
154
+ for (const file of rawFiles) {
155
+ attachmentMap[file.fieldname] = file.originalname;
156
+ }
157
+
158
+ const meta = buildRequestMeta(apireq.req);
159
+
160
+ const context = {
161
+ ...thevars,
162
+ _rcpt_email_: recipient,
163
+ _attachments_: attachmentMap,
164
+ _vars_: thevars,
165
+ _fields_: apireq.req.body,
166
+ _files_: rawFiles,
167
+ _meta_: meta
168
+ };
169
+
170
+ nunjucks.configure({ autoescape: true });
171
+ const html = nunjucks.renderString(form.template, context);
172
+
173
+ const mailOptions = {
174
+ from: form.sender,
175
+ to: recipient || form.recipient,
176
+ subject: form.subject,
177
+ html,
178
+ attachments
179
+ };
180
+
181
+ try {
182
+ const info = await this.server.storage.transport!.sendMail(mailOptions);
183
+ this.server.storage.print_debug('Email sent: ' + info.response);
184
+ } catch (error) {
185
+ const errorMessage = error instanceof Error ? error.message : String(error);
186
+ this.server.storage.print_debug('Error sending email: ' + errorMessage);
187
+ return [500, { error: `Error sending email: ${errorMessage}` }];
188
+ }
189
+
190
+ return [200, {}];
191
+ }
192
+
193
+ override defineRoutes(): ApiRoute[] {
194
+ return [
195
+ {
196
+ method: 'post',
197
+ path: '/v1/form/template',
198
+ handler: (req) => this.postFormTemplate(req as mailApiRequest),
199
+ auth: { type: 'yes', req: 'any' }
200
+ },
201
+ {
202
+ method: 'post',
203
+ path: '/v1/form/message',
204
+ handler: (req) => this.postSendForm(req),
205
+ auth: { type: 'none', req: 'any' }
206
+ }
207
+ ];
208
+ }
209
+ }
@@ -0,0 +1,242 @@
1
+ import { ApiModule, ApiRoute, ApiError } from '@technomoron/api-server-base';
2
+ import emailAddresses, { ParsedMailbox } from 'email-addresses';
3
+ import { convert } from 'html-to-text';
4
+ import nunjucks from 'nunjucks';
5
+
6
+ import { api_domain } from '../models/domain.js';
7
+ import { api_txmail } from '../models/txmail.js';
8
+ import { api_user } from '../models/user.js';
9
+ import { mailApiServer } from '../server.js';
10
+ import { buildRequestMeta } from '../util.js';
11
+
12
+ import type { mailApiRequest, UploadedFile } from '../types.js';
13
+
14
+ export class MailerAPI extends ApiModule<mailApiServer> {
15
+ //
16
+ // Validate and return the parsed email address
17
+ //
18
+ validateEmail(email: string): string | null {
19
+ const parsed = emailAddresses.parseOneAddress(email);
20
+ if (parsed) {
21
+ return (parsed as ParsedMailbox).address;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ //
27
+ // Validate a set of email addresses. Return arrays of invalid
28
+ // and valid email addresses.
29
+ //
30
+
31
+ validateEmails(list: string): { valid: string[]; invalid: string[] } {
32
+ const valid = [] as string[],
33
+ invalid = [] as string[];
34
+
35
+ const emails = list
36
+ .split(',')
37
+ .map((email) => email.trim())
38
+ .filter((email) => email !== '');
39
+ emails.forEach((email) => {
40
+ const addr = this.validateEmail(email);
41
+ if (addr) {
42
+ valid.push(addr);
43
+ } else {
44
+ invalid.push(email);
45
+ }
46
+ });
47
+ return { valid, invalid };
48
+ }
49
+
50
+ async assert_domain_and_user(apireq: mailApiRequest) {
51
+ const { domain, locale } = apireq.req.body;
52
+
53
+ if (!domain) {
54
+ throw new ApiError({ code: 401, message: 'Missing domain' });
55
+ }
56
+ const user = await api_user.findOne({ where: { token: apireq.token } });
57
+ if (!user) {
58
+ throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
59
+ }
60
+ const dbdomain = await api_domain.findOne({ where: { name: domain } });
61
+ if (!dbdomain) {
62
+ throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
63
+ }
64
+ apireq.domain = dbdomain;
65
+ apireq.locale = locale || 'en';
66
+ apireq.user = user;
67
+ }
68
+
69
+ // Store a template in the database
70
+
71
+ private async post_template(apireq: mailApiRequest): Promise<[number, { Status: string }]> {
72
+ await this.assert_domain_and_user(apireq);
73
+
74
+ const { template, sender = '', name, subject = '', locale = '' } = apireq.req.body;
75
+
76
+ if (!template) {
77
+ throw new ApiError({ code: 400, message: 'Missing template data' });
78
+ }
79
+ if (!name) {
80
+ throw new ApiError({ code: 400, message: 'Missing template name' });
81
+ }
82
+
83
+ const data = {
84
+ user_id: apireq.user!.user_id,
85
+ domain_id: apireq.domain!.domain_id,
86
+ name,
87
+ subject,
88
+ locale,
89
+ sender,
90
+ template
91
+ };
92
+
93
+ /*
94
+ console.log(JSON.stringify({
95
+ user: apireq.user,
96
+ domain: apireq.domain,
97
+ domain_id: apireq.domain.domain_id,
98
+ data
99
+ }, undefined, 2)); */
100
+
101
+ try {
102
+ const [templateRecord, created] = await api_txmail.upsert(data, {
103
+ returning: true
104
+ });
105
+ console.log('Template upserted:', templateRecord.name, 'Created:', created);
106
+ } catch (error: unknown) {
107
+ throw new ApiError({
108
+ code: 500,
109
+ message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error on upsert template')
110
+ });
111
+ }
112
+ return [200, { Status: 'OK' }];
113
+ }
114
+
115
+ // Send a template using posted arguments.
116
+
117
+ private async post_send(apireq: mailApiRequest): Promise<[number, Record<string, unknown>]> {
118
+ await this.assert_domain_and_user(apireq);
119
+
120
+ const { name, rcpt, domain = '', locale = '', vars = {} } = apireq.req.body;
121
+
122
+ if (!name || !rcpt || !domain) {
123
+ throw new ApiError({ code: 400, message: 'name/rcpt/domain required' });
124
+ }
125
+
126
+ let parsedVars: unknown = vars ?? {};
127
+ if (typeof vars === 'string') {
128
+ try {
129
+ parsedVars = JSON.parse(vars);
130
+ } catch (error) {
131
+ throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
132
+ }
133
+ }
134
+ const thevars = parsedVars as Record<string, unknown>;
135
+
136
+ // const dbdomain = await api_domain.findOne({ where: { domain } });
137
+
138
+ const { valid, invalid } = this.validateEmails(rcpt);
139
+ if (invalid.length > 0) {
140
+ throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
141
+ }
142
+ let template: api_txmail | null = null;
143
+ const deflocale = apireq.server.store.deflocale || '';
144
+ const domain_id = apireq.domain!.domain_id;
145
+
146
+ try {
147
+ template =
148
+ (await api_txmail.findOne({ where: { name, domain_id, locale } })) ||
149
+ (await api_txmail.findOne({ where: { name, domain_id, locale: deflocale } })) ||
150
+ (await api_txmail.findOne({ where: { name, domain_id } }));
151
+ } catch (error: unknown) {
152
+ throw new ApiError({
153
+ code: 500,
154
+ message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error')
155
+ });
156
+ }
157
+ if (!template) {
158
+ throw new ApiError({
159
+ code: 404,
160
+ message: `Template "${name}" not found for any locale in domain "${domain}"`
161
+ });
162
+ }
163
+
164
+ const sender = template.sender || apireq.domain!.sender || apireq.user!.email;
165
+ if (!sender) {
166
+ throw new ApiError({ code: 500, message: `Unable to locate sender for ${template.name}` });
167
+ }
168
+
169
+ const rawFiles = Array.isArray(apireq.req.files) ? (apireq.req.files as UploadedFile[]) : [];
170
+ const templateAssets = Array.isArray(template.files) ? template.files : [];
171
+ const attachments = [
172
+ ...templateAssets.map((file) => ({
173
+ filename: file.filename,
174
+ path: file.path,
175
+ cid: file.cid
176
+ })),
177
+ ...rawFiles.map((file) => ({
178
+ filename: file.originalname,
179
+ path: file.path
180
+ }))
181
+ ];
182
+
183
+ const attachmentMap: Record<string, string> = {};
184
+ for (const file of rawFiles) {
185
+ attachmentMap[file.fieldname] = file.originalname;
186
+ }
187
+ console.log(JSON.stringify({ vars, thevars }, undefined, 2));
188
+
189
+ const meta = buildRequestMeta(apireq.req);
190
+
191
+ try {
192
+ const env = new nunjucks.Environment(null, { autoescape: false });
193
+
194
+ const compiled = nunjucks.compile(template.template, env);
195
+
196
+ for (const recipient of valid) {
197
+ const fullargs = {
198
+ ...thevars,
199
+ _rcpt_email_: recipient,
200
+ _attachments_: attachmentMap,
201
+ _vars_: thevars,
202
+ _meta_: meta
203
+ };
204
+ const html = await compiled.render(fullargs);
205
+ const text = convert(html);
206
+ const sendargs = {
207
+ from: sender,
208
+ to: recipient,
209
+ subject: template.subject || apireq.req.body.subject || '',
210
+ html,
211
+ text,
212
+ attachments
213
+ };
214
+ await apireq.server.storage.transport.sendMail(sendargs);
215
+ }
216
+ return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
217
+ } catch (error: unknown) {
218
+ // console.log(JSON.stringify(e, null, 2));
219
+ throw new ApiError({
220
+ code: 500,
221
+ message: error instanceof Error ? error.message : String(error)
222
+ });
223
+ }
224
+ }
225
+
226
+ override defineRoutes(): ApiRoute[] {
227
+ return [
228
+ {
229
+ method: 'post',
230
+ path: '/v1/tx/message',
231
+ handler: this.post_send.bind(this),
232
+ auth: { type: 'yes', req: 'any' }
233
+ },
234
+ {
235
+ method: 'post',
236
+ path: '/v1/tx/template',
237
+ handler: this.post_template.bind(this),
238
+ auth: { type: 'yes', req: 'any' }
239
+ }
240
+ ];
241
+ }
242
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { pathToFileURL } from 'node:url';
2
+
3
+ import { FormAPI } from './api/forms.js';
4
+ import { MailerAPI } from './api/mailer.js';
5
+ import { mailApiServer } from './server.js';
6
+ import { mailStore } from './store/store.js';
7
+
8
+ import type { ApiServerConf } from '@technomoron/api-server-base';
9
+
10
+ export type MailMagicServerOptions = Partial<ApiServerConf>;
11
+
12
+ export type MailMagicServerBootstrap = {
13
+ server: mailApiServer;
14
+ store: mailStore;
15
+ env: mailStore['env'];
16
+ };
17
+
18
+ function buildServerConfig(store: mailStore, overrides: MailMagicServerOptions): MailMagicServerOptions {
19
+ const env = store.env;
20
+ return {
21
+ apiHost: env.API_HOST,
22
+ apiPort: env.API_PORT,
23
+ uploadPath: env.UPLOAD_PATH,
24
+ debug: env.DEBUG,
25
+ ...overrides
26
+ };
27
+ }
28
+
29
+ export async function createMailMagicServer(overrides: MailMagicServerOptions = {}): Promise<MailMagicServerBootstrap> {
30
+ const store = await new mailStore().init();
31
+ const config = buildServerConfig(store, overrides);
32
+ const server = new mailApiServer(config, store).api(new MailerAPI()).api(new FormAPI());
33
+
34
+ return { server, store, env: store.env };
35
+ }
36
+
37
+ export async function startMailMagicServer(overrides: MailMagicServerOptions = {}): Promise<MailMagicServerBootstrap> {
38
+ const bootstrap = await createMailMagicServer(overrides);
39
+ await bootstrap.server.start();
40
+ return bootstrap;
41
+ }
42
+
43
+ async function bootMailMagic() {
44
+ try {
45
+ const { env } = await startMailMagicServer();
46
+ console.log(`mail-magic server listening on ${env.API_HOST}:${env.API_PORT}`);
47
+ } catch (err) {
48
+ console.error('Failed to start FormMailer:', err);
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ const isDirectExecution = (() => {
54
+ if (!process.argv[1]) {
55
+ return false;
56
+ }
57
+
58
+ try {
59
+ return import.meta.url === pathToFileURL(process.argv[1]).href;
60
+ } catch {
61
+ return false;
62
+ }
63
+ })();
64
+
65
+ if (isDirectExecution) {
66
+ void bootMailMagic();
67
+ }
@@ -0,0 +1,112 @@
1
+ import { Sequelize } from 'sequelize';
2
+
3
+ import { mailStore } from './../store/store.js';
4
+ import { init_api_domain, api_domain } from './domain.js';
5
+ import { init_api_form, api_form } from './form.js';
6
+ import { importData } from './init.js';
7
+ import { init_api_txmail, api_txmail } from './txmail.js';
8
+ import { init_api_user, api_user } from './user.js';
9
+
10
+ import type { Dialect, Options } from 'sequelize';
11
+
12
+ export async function init_api_db(db: Sequelize, store: mailStore) {
13
+ await init_api_user(db);
14
+ await init_api_domain(db);
15
+ await init_api_txmail(db);
16
+ await init_api_form(db);
17
+
18
+ // User ↔ Domain
19
+ api_user.hasMany(api_domain, {
20
+ foreignKey: 'user_id',
21
+ as: 'domains'
22
+ });
23
+ api_domain.belongsTo(api_user, {
24
+ foreignKey: 'user_id',
25
+ as: 'user'
26
+ });
27
+
28
+ // User ↔ Template
29
+ api_user.hasMany(api_txmail, {
30
+ foreignKey: 'user_id',
31
+ as: 'txmail'
32
+ });
33
+ api_txmail.belongsTo(api_user, {
34
+ foreignKey: 'user_id',
35
+ as: 'user'
36
+ });
37
+
38
+ // Domain ↔ Template
39
+ api_domain.hasMany(api_txmail, {
40
+ foreignKey: 'domain_id',
41
+ as: 'txmail'
42
+ });
43
+ api_txmail.belongsTo(api_domain, {
44
+ foreignKey: 'domain_id',
45
+ as: 'domain'
46
+ });
47
+ api_user.belongsTo(api_domain, {
48
+ foreignKey: 'domain',
49
+ as: 'defaultDomain'
50
+ });
51
+ api_domain.hasMany(api_user, {
52
+ foreignKey: 'domain',
53
+ as: 'usersWithDefault'
54
+ });
55
+ api_user.hasMany(api_form, {
56
+ foreignKey: 'user_id',
57
+ as: 'forms'
58
+ });
59
+ api_form.belongsTo(api_user, {
60
+ foreignKey: 'user_id',
61
+ as: 'user'
62
+ });
63
+ api_domain.hasMany(api_form, {
64
+ foreignKey: 'domain_id',
65
+ as: 'forms'
66
+ });
67
+ api_form.belongsTo(api_domain, {
68
+ foreignKey: 'domain_id',
69
+ as: 'domain'
70
+ });
71
+
72
+ await db.query('PRAGMA foreign_keys = OFF');
73
+ store.print_debug(`Force alter tables: ${store.env.DB_FORCE_SYNC}`);
74
+ await db.sync({ alter: true, force: store.env.DB_FORCE_SYNC });
75
+ await db.query('PRAGMA foreign_keys = ON');
76
+
77
+ await importData(store);
78
+ store.print_debug('API Database Initialized...');
79
+ }
80
+
81
+ export async function connect_api_db(store: mailStore): Promise<Sequelize> {
82
+ console.log('DB INIT');
83
+
84
+ const env = store.env;
85
+ const dbparams: Options = {
86
+ logging: false, // env.DB_LOG ? console.log : false,
87
+ dialect: env.DB_TYPE as Dialect,
88
+ dialectOptions: {
89
+ charset: 'utf8mb4'
90
+ },
91
+ define: {
92
+ charset: 'utf8mb4',
93
+ collate: 'utf8mb4_unicode_ci'
94
+ }
95
+ };
96
+ if (env.DB_TYPE === 'sqlite') {
97
+ dbparams.storage = env.DB_NAME.endsWith('.db') ? env.DB_NAME : `${env.DB_NAME}.db`;
98
+ } else {
99
+ dbparams.host = env.DB_HOST;
100
+ dbparams.database = env.DB_NAME;
101
+ dbparams.username = env.DB_USER;
102
+ dbparams.password = env.DB_PASS;
103
+ }
104
+ store.print_debug(`Database params are:\n${JSON.stringify(dbparams, undefined, 2)}`);
105
+ const db = new Sequelize(dbparams);
106
+ await db.authenticate();
107
+
108
+ store.print_debug('API Database Connected');
109
+
110
+ await init_api_db(db, store);
111
+ return db;
112
+ }
@@ -0,0 +1,72 @@
1
+ import { Sequelize, Model, DataTypes } from 'sequelize';
2
+ import { z } from 'zod';
3
+
4
+ export const api_domain_schema = z.object({
5
+ domain_id: z.number().int().nonnegative(),
6
+ user_id: z.number().int().nonnegative(),
7
+ name: z.string().min(1),
8
+ sender: z.string().default(''),
9
+ locale: z.string().default(''),
10
+ is_default: z.boolean().default(false)
11
+ });
12
+
13
+ export type api_domain_type = z.infer<typeof api_domain_schema>;
14
+
15
+ export class api_domain extends Model {
16
+ declare domain_id: number;
17
+ declare user_id: number;
18
+ declare name: string;
19
+ declare sender: string;
20
+ declare locale: string;
21
+ declare is_default: boolean;
22
+ }
23
+
24
+ export async function init_api_domain(api_db: Sequelize): Promise<typeof api_domain> {
25
+ api_domain.init(
26
+ {
27
+ domain_id: {
28
+ type: DataTypes.INTEGER,
29
+ autoIncrement: true,
30
+ allowNull: false,
31
+ primaryKey: true
32
+ },
33
+ user_id: {
34
+ type: DataTypes.INTEGER,
35
+ allowNull: false,
36
+ references: {
37
+ model: 'user',
38
+ key: 'user_id'
39
+ },
40
+ onDelete: 'CASCADE',
41
+ onUpdate: 'CASCADE'
42
+ },
43
+ name: {
44
+ type: DataTypes.STRING,
45
+ allowNull: false,
46
+ defaultValue: ''
47
+ },
48
+ sender: {
49
+ type: DataTypes.STRING,
50
+ allowNull: false,
51
+ defaultValue: ''
52
+ },
53
+ locale: {
54
+ type: DataTypes.STRING,
55
+ allowNull: false,
56
+ defaultValue: ''
57
+ },
58
+ is_default: {
59
+ type: DataTypes.BOOLEAN,
60
+ allowNull: false,
61
+ defaultValue: false
62
+ }
63
+ },
64
+ {
65
+ sequelize: api_db,
66
+ tableName: 'domain',
67
+ charset: 'utf8mb4',
68
+ collate: 'utf8mb4_unicode_ci'
69
+ }
70
+ );
71
+ return api_domain;
72
+ }