@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,198 @@
1
+ import path from 'path';
2
+
3
+ import { Sequelize, Model, DataTypes } from 'sequelize';
4
+ import { z } from 'zod';
5
+
6
+ import { StoredFile } from '../types.js';
7
+ import { user_and_domain, normalizeSlug } from '../util.js';
8
+
9
+ export const api_form_schema = z.object({
10
+ form_id: z.number().int().nonnegative(),
11
+ user_id: z.number().int().nonnegative(),
12
+ domain_id: z.number().int().nonnegative(),
13
+ locale: z.string().default(''),
14
+ idname: z.string().min(1),
15
+ sender: z.string().min(1),
16
+ recipient: z.string().min(1),
17
+ subject: z.string(),
18
+ template: z.string().default(''),
19
+ filename: z.string().default(''),
20
+ slug: z.string().default(''),
21
+ secret: z.string().default(''),
22
+ files: z
23
+ .array(
24
+ z.object({
25
+ filename: z.string(),
26
+ path: z.string(),
27
+ cid: z.string().optional()
28
+ })
29
+ )
30
+ .default([])
31
+ });
32
+
33
+ export type api_form_type = z.infer<typeof api_form_schema>;
34
+
35
+ export class api_form extends Model {
36
+ declare form_id: number;
37
+ declare user_id: number;
38
+ declare domain_id: number;
39
+ declare locale: string;
40
+ declare idname: string;
41
+ declare sender: string;
42
+ declare recipient: string;
43
+ declare subject: string;
44
+ declare template: string;
45
+ declare filename: string;
46
+ declare slug: string;
47
+ declare secret: string;
48
+ declare files: StoredFile[];
49
+ }
50
+
51
+ export async function init_api_form(api_db: Sequelize): Promise<typeof api_form> {
52
+ api_form.init(
53
+ {
54
+ form_id: {
55
+ type: DataTypes.INTEGER,
56
+ autoIncrement: true,
57
+ allowNull: false,
58
+ primaryKey: true
59
+ },
60
+ user_id: {
61
+ type: DataTypes.INTEGER,
62
+ allowNull: false,
63
+ unique: false,
64
+ references: {
65
+ model: 'user',
66
+ key: 'user_id'
67
+ },
68
+ onDelete: 'CASCADE',
69
+ onUpdate: 'CASCADE'
70
+ },
71
+ domain_id: {
72
+ type: DataTypes.INTEGER,
73
+ allowNull: false,
74
+ unique: false,
75
+ references: {
76
+ model: 'domain',
77
+ key: 'domain_id'
78
+ },
79
+ onDelete: 'CASCADE',
80
+ onUpdate: 'CASCADE'
81
+ },
82
+ locale: {
83
+ type: DataTypes.STRING,
84
+ allowNull: false,
85
+ defaultValue: '',
86
+ unique: false
87
+ },
88
+ idname: {
89
+ type: DataTypes.STRING,
90
+ allowNull: false,
91
+ unique: false,
92
+ defaultValue: ''
93
+ },
94
+ sender: {
95
+ type: DataTypes.STRING,
96
+ allowNull: false,
97
+ defaultValue: ''
98
+ },
99
+ recipient: {
100
+ type: DataTypes.STRING,
101
+ allowNull: false,
102
+ defaultValue: ''
103
+ },
104
+ subject: {
105
+ type: DataTypes.STRING,
106
+ allowNull: false,
107
+ defaultValue: ''
108
+ },
109
+ filename: {
110
+ type: DataTypes.STRING,
111
+ allowNull: false,
112
+ defaultValue: ''
113
+ },
114
+ template: {
115
+ type: DataTypes.TEXT,
116
+ allowNull: false,
117
+ defaultValue: ''
118
+ },
119
+ slug: {
120
+ type: DataTypes.STRING,
121
+ allowNull: false,
122
+ defaultValue: ''
123
+ },
124
+ secret: {
125
+ type: DataTypes.STRING,
126
+ allowNull: false,
127
+ defaultValue: ''
128
+ },
129
+ files: {
130
+ type: DataTypes.TEXT,
131
+ allowNull: false,
132
+ defaultValue: '[]',
133
+ get() {
134
+ const raw = this.getDataValue('files') as string | null;
135
+ return raw ? (JSON.parse(raw) as StoredFile[]) : [];
136
+ },
137
+ set(value: StoredFile[] | null | undefined) {
138
+ this.setDataValue('files', JSON.stringify(value ?? []));
139
+ }
140
+ }
141
+ },
142
+ {
143
+ sequelize: api_db,
144
+ tableName: 'form',
145
+ charset: 'utf8mb4',
146
+ collate: 'utf8mb4_unicode_ci',
147
+ indexes: [
148
+ {
149
+ unique: true,
150
+ fields: ['user_id', 'domain_id', 'locale', 'idname']
151
+ }
152
+ ]
153
+ }
154
+ );
155
+
156
+ return api_form;
157
+ }
158
+
159
+ export async function upsert_form(record: api_form_type): Promise<api_form> {
160
+ const { user, domain } = await user_and_domain(record.domain_id);
161
+
162
+ const idname = normalizeSlug(user.idname);
163
+ const dname = normalizeSlug(domain.name);
164
+ const name = normalizeSlug(record.idname);
165
+ const locale = normalizeSlug(record.locale || domain.locale || user.locale || '');
166
+
167
+ if (!record.slug) {
168
+ record.slug = `${idname}-${dname}${locale ? '-' + locale : ''}-${name}`;
169
+ }
170
+
171
+ if (!record.filename) {
172
+ const parts = [idname, dname, 'form-template'];
173
+ if (locale) parts.push(locale);
174
+ parts.push(name);
175
+ record.filename = path.join(...parts);
176
+ } else {
177
+ record.filename = path.join(idname, dname, 'form-template', record.filename);
178
+ }
179
+ if (!record.filename.endsWith('.njk')) {
180
+ record.filename += '.njk';
181
+ }
182
+ record.filename = path.normalize(record.filename);
183
+
184
+ let instance: api_form | null = null;
185
+ instance = await api_form.findByPk(record.form_id);
186
+ if (instance) {
187
+ await instance.update(record);
188
+ } else {
189
+ console.log('CREATE', JSON.stringify(record, undefined, 2));
190
+
191
+ instance = await api_form.create(record);
192
+ console.log(`INSTANCE IS ${instance}`);
193
+ }
194
+ if (!instance) {
195
+ throw new Error(`Unable to update/create form ${record.form_id}`);
196
+ }
197
+ return instance;
198
+ }
@@ -0,0 +1,237 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { Unyuck } from '@technomoron/unyuck';
5
+ import { z } from 'zod';
6
+
7
+ import { mailStore } from '../store/store.js';
8
+ import { StoredFile } from '../types.js';
9
+ import { user_and_domain } from '../util.js';
10
+
11
+ import { api_domain, api_domain_schema } from './domain.js';
12
+ import { api_form_schema, api_form_type, upsert_form } from './form.js';
13
+ import { api_txmail_schema, api_txmail_type, upsert_txmail } from './txmail.js';
14
+ import { api_user, api_user_schema } from './user.js';
15
+
16
+ interface LoadedTemplate {
17
+ html: string;
18
+ assets: StoredFile[];
19
+ }
20
+
21
+ const init_data_schema = z.object({
22
+ user: z.array(api_user_schema).default([]),
23
+ domain: z.array(api_domain_schema).default([]),
24
+ template: z.array(api_txmail_schema).default([]),
25
+ form: z.array(api_form_schema).default([])
26
+ });
27
+
28
+ type InitData = z.infer<typeof init_data_schema>;
29
+
30
+ /**
31
+ * Resolve an asset file within ./config/<userid>/<domain>/<type>/assets
32
+ */
33
+ function resolveAsset(
34
+ basePath: string,
35
+ type: 'form-template' | 'tx-template',
36
+ domainName: string,
37
+ assetName: string,
38
+ locale?: string | null
39
+ ): string | null {
40
+ const searchPaths: string[] = [];
41
+
42
+ // always domain-scoped
43
+ if (locale) {
44
+ searchPaths.push(path.join(domainName, type, locale));
45
+ }
46
+ searchPaths.push(path.join(domainName, type));
47
+
48
+ // no domain fallback → do not leak assets between domains
49
+ // but allow locale fallbacks inside type
50
+ if (locale) {
51
+ searchPaths.push(path.join(type, locale));
52
+ }
53
+ searchPaths.push(type);
54
+
55
+ for (const p of searchPaths) {
56
+ const candidate = path.join(basePath, p, 'assets', assetName);
57
+ if (fs.existsSync(candidate)) {
58
+ return candidate;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function extractAndReplaceAssets(
65
+ html: string,
66
+ opts: {
67
+ basePath: string;
68
+ type: 'form-template' | 'tx-template';
69
+ domainName: string;
70
+ locale?: string | null;
71
+ apiUrl: string;
72
+ idname: string;
73
+ }
74
+ ): { html: string; assets: StoredFile[] } {
75
+ const regex = /src=["']asset\(['"]([^'"]+)['"](?:,\s*(true|false|[01]))?\)["']/g;
76
+
77
+ const assets: StoredFile[] = [];
78
+
79
+ const replacedHtml = html.replace(regex, (_m, relPath: string, inlineFlag?: string) => {
80
+ const fullPath = resolveAsset(opts.basePath, opts.type, opts.domainName, relPath, opts.locale ?? undefined);
81
+ if (!fullPath) {
82
+ throw new Error(`Missing asset "${relPath}"`);
83
+ }
84
+
85
+ const isInline = inlineFlag === 'true' || inlineFlag === '1';
86
+ const storedFile: StoredFile = {
87
+ filename: relPath,
88
+ path: fullPath,
89
+ cid: isInline ? relPath : undefined
90
+ };
91
+
92
+ assets.push(storedFile);
93
+
94
+ return isInline
95
+ ? `src="cid:${relPath}"`
96
+ : `src="${opts.apiUrl}/image/${opts.idname}/${opts.type}/` +
97
+ `${opts.domainName ? opts.domainName + '/' : ''}` +
98
+ `${opts.locale ? opts.locale + '/' : ''}` +
99
+ relPath +
100
+ '"';
101
+ });
102
+
103
+ return { html: replacedHtml, assets };
104
+ }
105
+
106
+ async function _load_template(
107
+ store: mailStore,
108
+ filename: string,
109
+ pathname: string,
110
+ user: api_user,
111
+ domain: api_domain,
112
+ locale: string | null,
113
+ type: 'form-template' | 'tx-template'
114
+ ): Promise<LoadedTemplate> {
115
+ const rootDir = path.join(store.configpath, user.idname, domain.name, type);
116
+
117
+ let relFile = filename;
118
+ const prefix = path.join(user.idname, domain.name, type) + path.sep;
119
+ if (filename.startsWith(prefix)) {
120
+ relFile = filename.slice(prefix.length);
121
+ }
122
+
123
+ const absPath = path.resolve(rootDir, pathname || '', relFile);
124
+
125
+ if (!absPath.startsWith(rootDir)) {
126
+ throw new Error(`Invalid template path "${filename}"`);
127
+ }
128
+ if (!fs.existsSync(absPath)) {
129
+ throw new Error(`Missing template file "${absPath}"`);
130
+ }
131
+
132
+ const raw = fs.readFileSync(absPath, 'utf8');
133
+ if (!raw.trim()) {
134
+ throw new Error(`Template file "${absPath}" is empty`);
135
+ }
136
+
137
+ try {
138
+ const baseUserPath = path.join(store.configpath, user.idname);
139
+ const templateKey = path.relative(baseUserPath, absPath);
140
+ if (!templateKey) {
141
+ throw new Error(`Unable to resolve template path for "${absPath}"`);
142
+ }
143
+
144
+ const processor = new Unyuck({ basePath: baseUserPath });
145
+ const merged = processor.flattenNoAssets(templateKey);
146
+
147
+ const { html, assets } = extractAndReplaceAssets(merged, {
148
+ basePath: path.join(store.configpath, user.idname),
149
+ type,
150
+ domainName: domain.name,
151
+ locale,
152
+ apiUrl: store.env.API_URL,
153
+ idname: user.idname
154
+ });
155
+
156
+ return { html, assets };
157
+ } catch (err) {
158
+ throw new Error(`Template "${absPath}" failed to preprocess: ${(err as Error).message}`);
159
+ }
160
+ }
161
+ export async function loadFormTemplate(store: mailStore, form: api_form_type): Promise<LoadedTemplate> {
162
+ const { user, domain } = await user_and_domain(form.domain_id);
163
+ const locale = form.locale || domain.locale || user.locale || null;
164
+
165
+ return _load_template(store, form.filename, '', user, domain, locale, 'form-template');
166
+ }
167
+
168
+ export async function loadTxTemplate(store: mailStore, template: api_txmail_type): Promise<LoadedTemplate> {
169
+ const { user, domain } = await user_and_domain(template.domain_id);
170
+ const locale = template.locale || domain.locale || user.locale || null;
171
+
172
+ return _load_template(store, template.filename, '', user, domain, locale, 'tx-template');
173
+ }
174
+
175
+ export async function importData(store: mailStore) {
176
+ const initfile = path.join(store.configpath, 'init-data.json');
177
+ if (fs.existsSync(initfile)) {
178
+ store.print_debug(`Loading init data from ${initfile}`);
179
+ const data = await fs.promises.readFile(initfile, 'utf8');
180
+
181
+ let records: InitData;
182
+ try {
183
+ records = init_data_schema.parse(JSON.parse(data));
184
+ } catch (err) {
185
+ store.print_debug(`Invalid init-data.json: ${err}`);
186
+ return;
187
+ }
188
+
189
+ const pendingUserDomains: Array<{ user_id: number; domain: number }> = [];
190
+ if (records.user) {
191
+ store.print_debug('Creating user records');
192
+ for (const record of records.user) {
193
+ const { domain, ...userWithoutDomain } = record;
194
+
195
+ await api_user.upsert({ ...userWithoutDomain, domain: null });
196
+ if (typeof domain === 'number') {
197
+ pendingUserDomains.push({ user_id: record.user_id, domain });
198
+ }
199
+ }
200
+ }
201
+ if (records.domain) {
202
+ store.print_debug('Creating domain records');
203
+ for (const record of records.domain) {
204
+ await api_domain.upsert(record);
205
+ }
206
+ }
207
+ if (pendingUserDomains.length) {
208
+ store.print_debug('Linking user default domains');
209
+ for (const entry of pendingUserDomains) {
210
+ await api_user.update({ domain: entry.domain }, { where: { user_id: entry.user_id } });
211
+ }
212
+ }
213
+ if (records.template) {
214
+ store.print_debug('Creating template records');
215
+ for (const record of records.template) {
216
+ const fixed = await upsert_txmail(record);
217
+ if (!fixed.template) {
218
+ const { html, assets } = await loadTxTemplate(store, fixed);
219
+ await fixed.update({ template: html, files: assets });
220
+ }
221
+ }
222
+ }
223
+ if (records.form) {
224
+ store.print_debug('Creating form records');
225
+ for (const record of records.form) {
226
+ const fixed = await upsert_form(record);
227
+ if (!fixed.template) {
228
+ const { html, assets } = await loadFormTemplate(store, fixed);
229
+ await fixed.update({ template: html, files: assets });
230
+ }
231
+ }
232
+ }
233
+ store.print_debug('Initdata upserted successfully.');
234
+ } else {
235
+ store.print_debug(`No init data file, tried ${initfile}`);
236
+ }
237
+ }
@@ -0,0 +1,199 @@
1
+ import path from 'path';
2
+
3
+ import { Sequelize, Model, DataTypes } from 'sequelize';
4
+ import { z } from 'zod';
5
+
6
+ import { StoredFile } from '../types.js';
7
+ import { user_and_domain, normalizeSlug } from '../util.js';
8
+
9
+ export const api_txmail_schema = z.object({
10
+ template_id: z.number().int().nonnegative(),
11
+ user_id: z.number().int().nonnegative(),
12
+ domain_id: z.number().int().nonnegative(),
13
+ name: z.string().min(1),
14
+ locale: z.string().default(''),
15
+ template: z.string().default(''),
16
+ filename: z.string().default(''),
17
+ sender: z.string().min(1),
18
+ subject: z.string(),
19
+ slug: z.string().default(''),
20
+ files: z
21
+ .array(
22
+ z.object({
23
+ filename: z.string(),
24
+ path: z.string(),
25
+ cid: z.string().optional()
26
+ })
27
+ )
28
+ .default([])
29
+ });
30
+
31
+ export type api_txmail_type = z.infer<typeof api_txmail_schema>;
32
+ export class api_txmail extends Model {
33
+ declare template_id: number;
34
+ declare user_id: number;
35
+ declare domain_id: number;
36
+ declare name: string;
37
+ declare locale: string;
38
+ declare template: string;
39
+ declare filename: string;
40
+ declare sender: string;
41
+ declare subject: string;
42
+ declare slug: string;
43
+ declare files: StoredFile[];
44
+ }
45
+
46
+ export async function upsert_txmail(record: api_txmail_type): Promise<api_txmail> {
47
+ const { user, domain } = await user_and_domain(record.domain_id);
48
+
49
+ const idname = normalizeSlug(user.idname);
50
+ const dname = normalizeSlug(domain.name);
51
+ const name = normalizeSlug(record.name);
52
+ const locale = normalizeSlug(record.locale || domain.locale || user.locale || '');
53
+
54
+ if (!record.slug) {
55
+ record.slug = `${idname}-${dname}${locale ? '-' + locale : ''}-${name}`;
56
+ }
57
+
58
+ if (!record.filename) {
59
+ const parts = [idname, dname, 'tx-template'];
60
+ if (locale) parts.push(locale);
61
+ parts.push(name);
62
+ record.filename = path.join(...parts);
63
+ } else {
64
+ record.filename = path.join(idname, dname, 'tx-template', record.filename);
65
+ }
66
+ if (!record.filename.endsWith('.njk')) {
67
+ record.filename += '.njk';
68
+ }
69
+ record.filename = path.normalize(record.filename);
70
+
71
+ const [instance] = await api_txmail.upsert(record);
72
+ return instance;
73
+ }
74
+
75
+ export async function init_api_txmail(api_db: Sequelize): Promise<typeof api_txmail> {
76
+ api_txmail.init(
77
+ {
78
+ template_id: {
79
+ type: DataTypes.INTEGER,
80
+ autoIncrement: true,
81
+ allowNull: false,
82
+ primaryKey: true
83
+ },
84
+ user_id: {
85
+ type: DataTypes.INTEGER,
86
+ allowNull: false,
87
+ unique: false,
88
+ references: {
89
+ model: 'user',
90
+ key: 'user_id'
91
+ },
92
+ onDelete: 'CASCADE',
93
+ onUpdate: 'CASCADE'
94
+ },
95
+ domain_id: {
96
+ type: DataTypes.INTEGER,
97
+ allowNull: false,
98
+ unique: false,
99
+ references: {
100
+ model: 'domain',
101
+ key: 'domain_id'
102
+ },
103
+ onDelete: 'CASCADE',
104
+ onUpdate: 'CASCADE'
105
+ },
106
+ name: {
107
+ type: DataTypes.STRING,
108
+ allowNull: false,
109
+ unique: false
110
+ },
111
+ locale: {
112
+ type: DataTypes.STRING,
113
+ allowNull: false,
114
+ defaultValue: '',
115
+ unique: false
116
+ },
117
+ template: {
118
+ type: DataTypes.STRING,
119
+ allowNull: false,
120
+ defaultValue: ''
121
+ },
122
+ filename: {
123
+ type: DataTypes.STRING,
124
+ allowNull: false,
125
+ defaultValue: ''
126
+ },
127
+ sender: {
128
+ type: DataTypes.STRING,
129
+ allowNull: false
130
+ },
131
+ subject: {
132
+ type: DataTypes.STRING,
133
+ allowNull: false,
134
+ defaultValue: ''
135
+ },
136
+ slug: {
137
+ type: DataTypes.STRING,
138
+ allowNull: false,
139
+ defaultValue: ''
140
+ },
141
+ part: {
142
+ type: DataTypes.BOOLEAN,
143
+ allowNull: false,
144
+ defaultValue: false
145
+ },
146
+ files: {
147
+ type: DataTypes.TEXT,
148
+ allowNull: false,
149
+ defaultValue: '[]',
150
+ get() {
151
+ const raw = this.getDataValue('files') as string | null;
152
+ return raw ? (JSON.parse(raw) as StoredFile[]) : [];
153
+ },
154
+ set(value: StoredFile[] | null | undefined) {
155
+ this.setDataValue('files', JSON.stringify(value ?? []));
156
+ }
157
+ }
158
+ },
159
+ {
160
+ sequelize: api_db,
161
+ tableName: 'txmail',
162
+ charset: 'utf8mb4',
163
+ collate: 'utf8mb4_unicode_ci',
164
+ indexes: [
165
+ {
166
+ unique: true,
167
+ fields: ['user_id', 'domain_id', 'locale', 'name']
168
+ }
169
+ ]
170
+ }
171
+ );
172
+
173
+ api_txmail.addHook('beforeValidate', async (template: api_txmail) => {
174
+ const { user, domain } = await user_and_domain(template.domain_id);
175
+
176
+ console.log('HERE');
177
+
178
+ const idname = normalizeSlug(user.idname);
179
+ const dname = normalizeSlug(domain.name);
180
+ const name = normalizeSlug(template.name);
181
+ const locale = normalizeSlug(template.locale || domain.locale || user.locale || '');
182
+
183
+ template.slug ||= `${idname}-${dname}${locale ? '-' + locale : ''}-${name}`;
184
+
185
+ if (!template.filename) {
186
+ const parts = [idname, dname, 'tx-template'];
187
+ if (locale) parts.push(locale);
188
+ parts.push(name);
189
+ template.filename = parts.join('/');
190
+ }
191
+ if (!template.filename.endsWith('.njk')) {
192
+ template.filename += '.njk';
193
+ }
194
+
195
+ console.log(`FILENAME IS: ${template.filename}`);
196
+ });
197
+
198
+ return api_txmail;
199
+ }
@@ -0,0 +1,79 @@
1
+ import { Sequelize, Model, DataTypes } from 'sequelize';
2
+ import { z } from 'zod';
3
+
4
+ export const api_user_schema = z.object({
5
+ user_id: z.number().int().nonnegative(),
6
+ idname: z.string().min(1),
7
+ token: z.string().min(1),
8
+ name: z.string().min(1),
9
+ email: z.string().email(),
10
+ domain: z.number().int().nonnegative().nullable().optional(),
11
+ locale: z.string().default('')
12
+ });
13
+
14
+ export type api_user_type = z.infer<typeof api_user_schema>;
15
+ export class api_user extends Model {
16
+ declare user_id: number;
17
+ declare idname: string;
18
+ declare token: string;
19
+ declare name: string;
20
+ declare email: string;
21
+ declare domain: number;
22
+ declare locale: string;
23
+ }
24
+
25
+ export async function init_api_user(api_db: Sequelize): Promise<typeof api_user> {
26
+ await api_user.init(
27
+ {
28
+ user_id: {
29
+ type: DataTypes.INTEGER,
30
+ autoIncrement: true,
31
+ allowNull: false,
32
+ primaryKey: true
33
+ },
34
+ idname: {
35
+ type: DataTypes.STRING,
36
+ allowNull: false,
37
+ defaultValue: ''
38
+ },
39
+ token: {
40
+ type: DataTypes.STRING,
41
+ allowNull: false,
42
+ defaultValue: ''
43
+ },
44
+ name: {
45
+ type: DataTypes.STRING,
46
+ allowNull: false,
47
+ defaultValue: ''
48
+ },
49
+ email: {
50
+ type: DataTypes.STRING,
51
+ allowNull: false,
52
+ defaultValue: ''
53
+ },
54
+ domain: {
55
+ type: DataTypes.INTEGER,
56
+ allowNull: true,
57
+ references: {
58
+ model: 'domain',
59
+ key: 'domain_id'
60
+ },
61
+ onDelete: 'SET NULL',
62
+ onUpdate: 'CASCADE',
63
+ defaultValue: null
64
+ },
65
+ locale: {
66
+ type: DataTypes.STRING,
67
+ allowNull: false,
68
+ defaultValue: ''
69
+ }
70
+ },
71
+ {
72
+ sequelize: api_db,
73
+ tableName: 'user',
74
+ charset: 'utf8mb4',
75
+ collate: 'utf8mb4_unicode_ci'
76
+ }
77
+ );
78
+ return api_user;
79
+ }