Skip to content

Commit ebde25f

Browse files
authored
Merge branch 'next' into feature/AdminForth/1193/i-think-we-had-some-feature-wh
2 parents 04f8e16 + 0402ac9 commit ebde25f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1913
-798
lines changed

adapters/install-adapters.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ adminforth-email-adapter-mailgun adminforth-google-oauth-adapter adminforth-gith
44
adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter \
55
adminforth-twitch-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 \
66
adminforth-storage-adapter-local adminforth-image-vision-adapter-openai adminforth-key-value-adapter-ram \
7-
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha adminforth-completion-adapter-google-gemini"
7+
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha adminforth-completion-adapter-google-gemini \
8+
adminforth-key-value-adapter-redis adminforth-key-value-adapter-leveldb"
89

910
# for each
1011
install_adapter() {
@@ -16,7 +17,7 @@ install_adapter() {
1617
git pull
1718
else
1819
echo "Repository for $adapter does not exist. Cloning..."
19-
git clone git@github.com:devforth/$adapter.git "$adapter"
20+
git clone https://github.com/devforth/$adapter.git "$adapter"
2021
cd "$adapter"
2122
fi
2223

adminforth/commands/createApp/templates/.env.local.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
ADMINFORTH_SECRET=123
22
NODE_ENV=development
3+
DEBUG_LEVEL=info
4+
AF_DEBUG_LEVEL=info
5+
DB_DEBUG_LEVEL=info
36
DATABASE_URL={{{dbUrl}}}
47
{{#if prismaDbUrl}}
58
PRISMA_DATABASE_URL={{{prismaDbUrl}}}
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { Express } from "express";
1+
import { Express, Request, Response } from "express";
22
import { IAdminForth } from "adminforth";
3-
43
export function initApi(app: Express, admin: IAdminForth) {
54
app.get(`${admin.config.baseUrl}/api/hello/`,
6-
(req, res) => {
7-
res.json({ message: "Hello from AdminForth API!" });
5+
async (req: Request, res: Response) => {
6+
const allUsers = await admin.resource("adminuser").list([]);
7+
res.json({
8+
message: "Hello from AdminForth API!",
9+
users: allUsers,
10+
});
811
}
912
);
1013
}

adminforth/dataConnectors/postgres.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
232232
}
233233
return JSON.stringify(value);
234234
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
235-
return value === null ? null : (value ? 1 : 0);
235+
return value === null ? null : (value ? true : false);
236236
} else if (field.type == AdminForthDataTypes.JSON) {
237237
if (field._underlineType == 'json') {
238238
return typeof value === 'string' || value === null ? value : JSON.stringify(value);
@@ -385,7 +385,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
385385
const tableName = resource.table;
386386
const result = {};
387387
await Promise.all(columns.map(async (col) => {
388-
const q = `SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM "${tableName}"`;
388+
const q = `SELECT MIN("${col.name}") as min, MAX("${col.name}") as max FROM "${tableName}"`;
389389
dbLogger.trace(`🪲📜 PG Q: ${q}`);
390390
const stmt = await this.client.query(q);
391391
const { min, max } = stmt.rows[0];

adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,100 @@ Now you can use this component in the configuration of the resource:
233233
}
234234
```
235235
236+
### Custom record editing (updating other fields)
237+
238+
Sometimes a custom editor needs to update not only its own field, but also other fields of the record (for example, generate a slug from a title).
239+
240+
For this, custom `edit`/`create` components can emit an `update:recordFieldValue` event with the payload `{ fieldName, fieldValue }`. AdminForth will update the corresponding field in the record.
241+
242+
> If you emit `update:recordFieldValue` to modify a field which is hidden by `showIn.create: false` / `showIn.edit: false`, the backend will reject the request by default.
243+
> To allow this, set the target column config to `allowModifyWhenNotShowInCreate: true` and/or `allowModifyWhenNotShowInEdit: true`.
244+
245+
```html title='./custom/TitleWithSlugEditor.vue'
246+
<template>
247+
<div class="flex flex-col gap-2">
248+
<Input
249+
:model-value="record[column.name]"
250+
:placeholder="$t('Title')"
251+
@update:model-value="onTitleChange"
252+
/>
253+
<Input
254+
:model-value="record.slug"
255+
:placeholder="$t('Slug')"
256+
readonly
257+
/>
258+
</div>
259+
</template>
260+
261+
<script setup lang="ts">
262+
import Input from "@/afcl/Input.vue";
263+
import type {
264+
AdminForthResourceColumnCommon,
265+
AdminForthResourceCommon,
266+
AdminUser,
267+
} from "@/types/Common";
268+
269+
const props = defineProps<{
270+
column: AdminForthResourceColumnCommon;
271+
record: any;
272+
meta: any;
273+
resource: AdminForthResourceCommon;
274+
adminUser: AdminUser;
275+
readonly: boolean;
276+
}>();
277+
278+
const emit = defineEmits([
279+
"update:value", // update current column value
280+
"update:recordFieldValue", // update any other field in the record
281+
]);
282+
283+
function slugify(value: string) {
284+
return value
285+
?.toLowerCase()
286+
.trim()
287+
.replace(/[^a-z0-9]+/g, "-")
288+
.replace(/(^-|-$)+/g, "");
289+
}
290+
291+
function onTitleChange(newTitle: string) {
292+
// update current column value
293+
emit("update:value", newTitle);
294+
295+
// update another field in the record (e.g. slug)
296+
emit("update:recordFieldValue", {
297+
fieldName: "slug",
298+
fieldValue: slugify(newTitle),
299+
});
300+
}
301+
</script>
302+
```
303+
304+
And use it in the resource configuration for both `edit` and `create` views:
305+
306+
```ts title='./resources/apartments.ts'
307+
{
308+
...
309+
resourceId: 'aparts',
310+
columns: [
311+
...
312+
{
313+
name: 'title',
314+
components: {
315+
edit: '@@/TitleWithSlugEditor.vue',
316+
create: '@@/TitleWithSlugEditor.vue',
317+
},
318+
},
319+
{
320+
name: 'slug',
321+
// standard input; value will be kept in sync
322+
...
323+
},
324+
...
325+
],
326+
...
327+
}
328+
```
329+
236330
### Custom inValidity inside of the custom create/edit components
237331
238332
Custom componets can emit `update:inValidity` event to parent to say that the field is invalid.

adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ When user opens edit page, AdminForth makes a request to the backend to get the
4444

4545
Practically you can use `show.afterDatasourceResponse` to modify or add some data before it is displayed on the edit page.
4646

47-
For example [upload plugin](/docs/tutorial/Plugins/upload/) uses this hook to generate signed preview URL so user can see existing uploaded file preview in form, and at the same time database stores only original file path which might be not accessible without presigned URL.
47+
For example [upload plugin](/docs/tutorial/Plugins/05-0-upload/) uses this hook to generate signed preview URL so user can see existing uploaded file preview in form, and at the same time database stores only original file path which might be not accessible without presigned URL.
4848

4949
## Saving data on edit page
5050

adminforth/documentation/docs/tutorial/03-Customization/12-security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Also you can add custom rules. For example to prevent popular words:
6666
],
6767
```
6868
69-
All rules defined in password column will be also delivered to [password reset plugin](../07-Plugins/07-email-password-reset.md) if you are using it to ensure that password reset will also respect same rules.
69+
All rules defined in password column will be also delivered to [password reset plugin](../08-Plugins/07-email-password-reset.md) if you are using it to ensure that password reset will also respect same rules.
7070
7171
7272
## Trusting client IP addresses

adminforth/documentation/docs/tutorial/05-ListOfAdapters.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,52 @@ The RAM adapter is a simplest in-memory key-value storage. Stores data in proces
244244

245245
Pros:
246246
* Simplest in use - does not reqauire any external daemon.
247+
247248
Cones:
248249
* In production sutable for single-process installations only
249250

250251

252+
### Redis adapter
253+
254+
```bash
255+
npm i @adminforth/key-value-adapter-redis
256+
```
257+
258+
Redis adapter uses in-memory RAM-based Redis database with O(1) get complexity. It is great fit for most of lightweight tasks which fit in RAM. Also capable with multi-process or replica-based installations as centralized storage. Please note that Redis daemon might be not persisted to disk during restarts without additional settings, so if persistence is critical for your task - you might need to set up it separately (for many tasks like rate-limits ephemeral data are fine
259+
260+
261+
```ts
262+
import RedisKeyValueAdapter from '@adminforth/key-value-adapter-redis';
263+
264+
const adapter = new RedisKeyValueAdapter({
265+
redisUrl: '127.0.0.1:6379'
266+
})
267+
268+
adapeter.set('test-key', 'test-value', 120); //expiry in 120 seconds
269+
270+
```
271+
272+
### LevelDB adapter
273+
274+
```bash
275+
npm i @adminforth/key-value-adapter-leveldb
276+
```
277+
278+
LebelDB uses disk storage with o(log(n)) get complexity. Good fit for large and/or persistent KV datasets which still require fast KV access but don't efficently fit into RAM. Please not that this is a single-process adapter only, so if you will run severall processes of admin - they will not be able to work with this adapter (>=2 processes which look at same level database might lead to unpredicted behaviour - exceptions or crashes).
279+
280+
You can use replicas with isolated disks, however in this case state will be also separated between replicas.
281+
282+
```ts
283+
import LevelDBKeyValueAdapter from '@adminforth/key-value-adapter-leveldb'
284+
285+
const adapter = new LevelDBKeyValueAdapter({
286+
dbPath: './testdb'
287+
});
288+
289+
adapeter.set('test-key', 'test-value', 120); //expiry in 120 seconds
290+
291+
```
292+
251293
## 🤖Captcha adapters
252294

253295
Used to add capthca to the login screen

adminforth/documentation/docs/tutorial/08-Plugins/02-TwoFactorsAuth.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,6 @@ options: {
280280
adminUser: adminUser,
281281
//diff-add
282282
userPk: adminUser.pk,
283-
//diff-add
284-
cookies: extra.cookies,
285-
//diff-add
286-
response: response,
287283
//diff-add
288284
extra: extra,
289285
//diff-add
@@ -393,8 +389,6 @@ hooks: {
393389
const verifyRes = await t2fa.verify(confirmationResult, {
394390
adminUser,
395391
userPk: adminUser.pk,
396-
cookies: extra?.cookies,
397-
response,
398392
extra
399393
});
400394
if (!verifyRes || 'error' in verifyRes) {
@@ -404,7 +398,7 @@ hooks: {
404398
},
405399
},
406400
create: {
407-
beforeSave: async ({ adminUser, adminforth, extra }) => {
401+
beforeSave: async ({ adminUser, adminforth, response, extra }) => {
408402
const t2fa = adminforth.getPluginByClassName('TwoFactorsAuthPlugin');
409403
if (!t2fa) {
410404
return { ok: false, error: 'TwoFactorsAuthPlugin is not configured' };
@@ -418,7 +412,7 @@ hooks: {
418412
const verifyRes = await t2fa.verify(confirmationResult, {
419413
adminUser,
420414
userPk: adminUser.pk,
421-
cookies: extra?.cookies,
415+
extra
422416
});
423417
if (!verifyRes || 'error' in verifyRes) {
424418
return { ok: false, error: verifyRes?.error || 'Two-factor verification failed' };
@@ -534,12 +528,8 @@ app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
534528
adminUser: adminUser,
535529
// diff-add
536530
userPk: adminUser.pk,
537-
// diff-add
538-
cookies: cookies,
539-
//diff-add
540-
response: res,
541531
//diff-add
542-
extra: {...req.headers},
532+
extra: {...req.headers, ...req.cookies, response: res},
543533
//diff-add
544534
});
545535
// diff-add

adminforth/documentation/docs/tutorial/08-Plugins/05-upload.md renamed to adminforth/documentation/docs/tutorial/08-Plugins/05-0-upload.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,4 +659,9 @@ And finally add this callback:
659659

660660
```
661661
662-
And now you can easily update avatar for each user
662+
And now you can easily update avatar for each user
663+
664+
# API
665+
666+
If you wish to reuse Upload plugin for advanced tasks like upload custom files from frontend/backend
667+
(including presigned frontend upload), see [05-1-upload-api.md](05-1-upload-api.md).

0 commit comments

Comments
 (0)