Skip to content

Commit 21c4190

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth
2 parents 4e7a06f + 6552081 commit 21c4190

File tree

20 files changed

+380
-40
lines changed

20 files changed

+380
-40
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,16 @@ npm run migrate
6868
npm start
6969
```
7070

71+
Add some columns to a database. Open .prisma file, modify it, and run:
72+
73+
```
74+
npm run migrate -- --name desctiption_of_changes
75+
```
76+
77+
Add some columns to a database. Open .prisma file, modify it, and run:
78+
79+
```
80+
npm run migrate -- --name desctiption_of_changes
81+
```
7182

7283

adminforth/documentation/blog/2024-11-14-compose-ec2-deployment-ci/index.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -331,18 +331,6 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state"
331331
}
332332
}
333333
334-
# DynamoDB table for state locking
335-
resource "aws_dynamodb_table" "terraform_lock" {
336-
name = "${local.app_name}-terraform-lock-table"
337-
billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand
338-
339-
hash_key = "LockID" # Primary key for the table
340-
341-
attribute {
342-
name = "LockID"
343-
type = "S"
344-
}
345-
}
346334
347335
```
348336

adminforth/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,6 @@ class AdminForth implements IAdminForth {
233233
this.validateFieldGroups(res.options.createFieldGroups, fieldTypes);
234234
this.validateFieldGroups(res.options.editFieldGroups, fieldTypes);
235235

236-
this.configValidator.postProcessAfterDiscover(res);
237236

238237
// check if primaryKey column is present
239238
if (!res.columns.some((col) => col.primaryKey)) {
@@ -244,6 +243,10 @@ class AdminForth implements IAdminForth {
244243

245244
this.statuses.dbDiscover = 'done';
246245

246+
for (const res of this.config.resources) {
247+
this.configValidator.postProcessAfterDiscover(res);
248+
}
249+
247250
this.operationalResources = {};
248251
this.config.resources.forEach((resource) => {
249252
this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);

adminforth/modules/codeInjector.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { exec, spawn } from 'child_process';
2-
import filewatcher from 'filewatcher';
2+
import filewatcher from 'filewatcher'; // todo - replace this legacy (>8y old module) with chokidar
33
import fs from 'fs';
44
import fsExtra from 'fs-extra';
55
import os from 'os';
@@ -349,6 +349,8 @@ class CodeInjector implements ICodeInjector {
349349
await fsExtra.copy(src, to, {
350350
recursive: true,
351351
dereference: true,
352+
// exclue if node_modules comes after /custom/ in path
353+
filter: (src) => !src.includes('/custom/node_modules'),
352354
});
353355
}
354356

@@ -744,9 +746,15 @@ class CodeInjector implements ICodeInjector {
744746

745747
const cwd = this.spaTmpPath();
746748

749+
750+
await this.runNpmShell({command: 'run i18n:extract', cwd});
751+
747752
if (!hotReload) {
748753
// probably add option to build with tsh check (plain 'build')
749754
const serveDir = this.getServeDir();
755+
756+
757+
750758
await this.runNpmShell({command: 'run build-only', cwd});
751759
// remove serveDir if exists
752760
try {
@@ -759,6 +767,7 @@ class CodeInjector implements ICodeInjector {
759767
await fsExtra.copy(path.join(cwd, 'dist'), serveDir, { recursive: true });
760768

761769
} else {
770+
762771
const command = 'run dev';
763772
console.log(`⚙️ spawn: npm ${command}...`);
764773
const nodeBinary = process.execPath;

adminforth/modules/configValidator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export default class ConfigValidator implements IConfigValidator {
119119
Object.keys(this.inputConfig.customization.loginPageInjections).forEach((injection) => {
120120
if (ALLOWED_LOGIN_INJECTIONS.includes(injection)) {
121121
loginPageInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.loginPageInjections, injection, errors);
122-
console.log('🐛🐛 loginPageInjections', JSON.stringify(loginPageInjections, null, 2));
123122
} else {
124123
const similar = suggestIfTypo(ALLOWED_LOGIN_INJECTIONS, injection);
125124
errors.push(`Login page injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_LOGIN_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
@@ -325,7 +324,7 @@ export default class ConfigValidator implements IConfigValidator {
325324
res.columns = [];
326325
}
327326
res.columns = res.columns.map((inCol: AdminForthResourceColumnInputCommon) => {
328-
const col: Partial<AdminForthResourceColumn> = { ...inCol };
327+
const col: Partial<AdminForthResourceColumn> = { ...inCol, required: undefined, editingNote: undefined };
329328

330329
col.label = col.label || guessLabelFromName(col.name);
331330
//define default sortable

adminforth/plugins/i18n/index.ts

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,97 @@
11
import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
22
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction } from "adminforth";
33
import type { PluginOptions } from './types.js';
4-
import iso6391 from 'iso-639-1';
4+
import iso6391, { LanguageCode } from 'iso-639-1';
5+
import path from 'path';
6+
import fs from 'fs-extra';
7+
import chokidar from 'chokidar';
8+
9+
interface ICachingAdapter {
10+
get(key: string): Promise<any>;
11+
set(key: string, value: any): Promise<void>;
12+
clear(key: string): Promise<void>;
13+
}
14+
15+
class CachingAdapterMemory implements ICachingAdapter {
16+
cache: Record<string, any> = {};
17+
async get(key: string) {
18+
return this.cache[key];
19+
}
20+
async set(key: string, value: any) {
21+
this.cache[key] = value;
22+
}
23+
async clear(key: string) {
24+
delete this.cache[key];
25+
}
26+
}
27+
528

629
export default class OpenSignupPlugin extends AdminForthPlugin {
730
options: PluginOptions;
831
emailField: AdminForthResourceColumn;
932
passwordField: AdminForthResourceColumn;
1033
authResource: AdminForthResource;
1134
emailConfirmedField?: AdminForthResourceColumn;
12-
35+
trFieldNames: Partial<Record<LanguageCode, string>>;
36+
enFieldName: string;
37+
cache: ICachingAdapter;
38+
1339
adminforth: IAdminForth;
1440

1541
constructor(options: PluginOptions) {
1642
super(options, import.meta.url);
1743
this.options = options;
44+
this.cache = new CachingAdapterMemory();
45+
this.trFieldNames = {};
1846
}
1947

2048
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
2149
super.modifyResourceConfig(adminforth, resourceConfig);
2250

2351
// check each supported language is valid ISO 639-1 code
2452
this.options.supportedLanguages.forEach((lang) => {
25-
console.log('lang', lang);
2653
if (!iso6391.validate(lang)) {
2754
throw new Error(`Invalid language code ${lang}, please define valid ISO 639-1 language code (2 lowercase letters)`);
2855
}
2956
});
3057

3158

59+
// parse trFieldNames
60+
for (const lang of this.options.supportedLanguages) {
61+
if (lang === 'en') {
62+
continue;
63+
}
64+
if (this.options.translationFieldNames?.[lang]) {
65+
this.trFieldNames[lang] = this.options.translationFieldNames[lang];
66+
} else {
67+
this.trFieldNames[lang] = lang + '_string';
68+
}
69+
// find column by name
70+
const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
71+
if (!column) {
72+
throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
73+
in resource ${resourceConfig.resourceId}, consider adding it to columns or change trFieldNames option to remap it to existing column`);
74+
}
75+
}
76+
77+
this.enFieldName = this.trFieldNames['en'] || 'en_string';
3278

79+
// if not enFieldName column is not found, throw error
80+
const enColumn = resourceConfig.columns.find(c => c.name === this.enFieldName);
81+
if (!enColumn) {
82+
throw new Error(`Field ${this.enFieldName} not found column to store english original string in resource ${resourceConfig.resourceId}`);
83+
}
84+
// for faster performance it should be a primary key
85+
if (!enColumn.primaryKey) {
86+
throw new Error(`Field ${this.enFieldName} should be primary key in resource ${resourceConfig.resourceId}`);
87+
}
88+
89+
// if sourceFieldName defined, check it exists
90+
if (this.options.sourceFieldName) {
91+
if (!resourceConfig.columns.find(c => c.name === this.options.sourceFieldName)) {
92+
throw new Error(`Field ${this.options.sourceFieldName} not found in resource ${resourceConfig.resourceId}`);
93+
}
94+
}
3395

3496
// add underLogin component
3597
(adminforth.config.customization.loginPageInjections.underInputs as AdminForthComponentDeclaration[]).push({
@@ -48,9 +110,78 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
48110
});
49111

50112
}
113+
114+
async processExtractedMessages(adminforth: IAdminForth, filePath: string) {
115+
// messages file is in i18n-messages.json
116+
let messages;
117+
try {
118+
messages = await fs.readJson(filePath);
119+
process.env.HEAVY_DEBUG && console.info('🐛 Messages file found');
120+
121+
} catch (e) {
122+
process.env.HEAVY_DEBUG && console.error('🐛 Messages file not yet exists, probably npm run i18n:extract not finished/started yet, might be ok');
123+
return;
124+
}
125+
console.log('🪲messages', messages);
126+
// loop over missingKeys[i].path and add them to database if not exists
127+
await Promise.all(messages.missingKeys.map(async (missingKey: any) => {
128+
const key = missingKey.path;
129+
const file = missingKey.file;
130+
const source = 'frontend';
131+
const exists = await adminforth.resource(this.resourceConfig.resourceId).count(Filters.EQ(this.enFieldName, key));
132+
console.log('🪲exists', exists);
133+
if (exists) {
134+
return;
135+
}
136+
const record = {
137+
[this.enFieldName]: key,
138+
...(this.options.sourceFieldName ? { [this.options.sourceFieldName]: `${source}:${file}` } : {}),
139+
};
140+
await adminforth.resource(this.resourceConfig.resourceId).create(record);
141+
}))
142+
143+
144+
}
145+
146+
147+
async tryProcessAndWatch(adminforth: IAdminForth) {
148+
const spaDir = adminforth.codeInjector.spaTmpPath();
149+
// messages file is in i18n-messages.json
150+
const messagesFile = path.join(spaDir, '..', 'spa_tmp', 'i18n-messages.json');
151+
console.log('🪲messagesFile', messagesFile);
152+
this.processExtractedMessages(adminforth, messagesFile);
153+
// we use watcher because file can't be yet created when we start - bundleNow can be done in build time or can be done now
154+
// that is why we make attempt to process it now and then watch for changes
155+
const w = chokidar.watch(messagesFile, { persistent: true });
156+
w.on('change', () => {
157+
this.processExtractedMessages(adminforth, messagesFile);
158+
});
159+
w.on('add', () => {
160+
this.processExtractedMessages(adminforth, messagesFile);
161+
});
162+
163+
}
51164

52165
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
53166
// optional method where you can safely check field types after database discovery was performed
167+
// ensure each trFieldName (apart from enFieldName) is nullable column of type string
168+
for (const lang of this.options.supportedLanguages) {
169+
if (lang === 'en') {
170+
continue;
171+
}
172+
const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
173+
if (!column) {
174+
throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
175+
in resource ${resourceConfig.resourceId}, consider adding it to columns or change trFieldNames option to remap it to existing column`);
176+
}
177+
if (column.required.create || column.required.edit) {
178+
throw new Error(`Field ${this.trFieldNames[lang]} should be not required in resource ${resourceConfig.resourceId}`);
179+
}
180+
}
181+
182+
// in this plugin we will use plugin to fill the database with missing language messages
183+
this.tryProcessAndWatch(adminforth);
184+
54185
}
55186

56187
instanceUniqueRepresentation(pluginOptions: any) : string {

adminforth/plugins/i18n/package-lock.json

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adminforth/plugins/i18n/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"description": "",
1616
"dependencies": {
1717
"@aws-sdk/client-ses": "^3.654.0",
18+
"chokidar": "^4.0.1",
1819
"iso-639-1": "^3.1.3"
1920
},
2021
"devDependencies": {

adminforth/plugins/i18n/types.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { EmailAdapter } from 'adminforth';
2-
import isoCountries from 'i18n-iso-countries';
2+
import type { LanguageCode } from 'iso-639-1';
33

4-
type Lang = keyof typeof isoCountries.langs();
54

65
export interface PluginOptions {
76

8-
supportedLanguages: Lang[]
9-
fieldNames: {
10-
[key: string]: string;
11-
}
7+
/* List of ISO 639-1 language codes which you want to tsupport*/
8+
supportedLanguages: LanguageCode[];
9+
10+
/**
11+
* Each translation string will be stored in a separate field, you can remap it to existing columns using this option
12+
* By default it will assume field are named like `${lang_code}_string` (e.g. 'en_string', 'uk_string', 'ja_string', 'fr_string')
13+
*/
14+
translationFieldNames: Partial<Record<LanguageCode, string>>;
15+
16+
/**
17+
* Optional field name for storing source language file name
18+
*/
19+
sourceFieldName?: string;
1220

1321
}

0 commit comments

Comments
 (0)