Skip to content

Commit 0ce5ca3

Browse files
authored
Merge pull request #485 from devforth/next
Next
2 parents ac249e7 + 90e7318 commit 0ce5ca3

File tree

17 files changed

+198
-69
lines changed

17 files changed

+198
-69
lines changed

adminforth/auth.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ getClientIp(headers: object) {
106106
}
107107

108108
setCustomCookie({ response, payload }: {
109-
response: any, payload: { name: string, value: string, expiry?: number | undefined, expirySeconds: number | undefined, httpOnly: boolean }
109+
response: any, payload: { name: string, value: string, expiry?: number | undefined, expirySeconds: number | undefined, httpOnly: boolean, sessionBased?: boolean | undefined }
110110
}) {
111-
const {name, value, expiry, httpOnly, expirySeconds } = payload;
111+
const {name, value, expiry, httpOnly, expirySeconds, sessionBased } = payload;
112112

113113
let expiryMs = 24 * 60 * 60 * 1000; // default 1 day
114114
if (expirySeconds !== undefined) {
@@ -117,11 +117,14 @@ getClientIp(headers: object) {
117117
afLogger.warn(`setCustomCookie: expiry(in ms) is deprecated, use expirySeconds instead (seconds), traceback: ${new Error().stack}`);
118118
expiryMs = expiry;
119119
}
120-
121120
const brandSlug = this.adminforth.config.customization.brandNameSlug;
122-
response.setHeader('Set-Cookie', `adminforth_${brandSlug}_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'};${
123-
httpOnly ? ' HttpOnly;' : ''
124-
} SameSite=Strict; Expires=${new Date(Date.now() + expiryMs).toUTCString() } `);
121+
response.setHeader('Set-Cookie',
122+
`adminforth_${brandSlug}_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'};${
123+
httpOnly ? ' HttpOnly;' : ''
124+
}SameSite=Strict;${
125+
sessionBased ? '' : `Expires=${new Date(Date.now() + expiryMs).toUTCString() }`
126+
}`
127+
);
125128
}
126129

127130
getCustomCookie({ cookies, name }: {

adminforth/commands/createPlugin/templates/index.ts.hbs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default class {{pluginName}} extends AdminForthPlugin {
99
constructor(options: PluginOptions) {
1010
super(options, import.meta.url);
1111
this.options = options;
12+
this.shouldHaveSingleInstancePerWholeApp = () => false;
1213
}
1314

1415
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
@@ -27,10 +28,6 @@ export default class {{pluginName}} extends AdminForthPlugin {
2728
return `single`;
2829
}
2930

30-
shouldHaveSingleInstancePerWholeApp(): boolean {
31-
return false;
32-
}
33-
3431
setupEndpoints(server: IHttpServer) {
3532
server.endpoint({
3633
method: 'POST',

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,45 @@ new CompletionAdapterGoogleGemini({
211211
}),
212212
```
213213

214+
### Using json_schema with adapter
215+
216+
If you want use custom json schema for completion response - you can use outputSchema param for completion:
217+
218+
219+
```ts
220+
const openAi = new CompletionAdapterOpenAIChatGPT({
221+
openAiApiKey: process.env.OPENAI_API_KEY as string,
222+
model: 'gpt-5-mini',
223+
});
224+
225+
const prompt = 'What is the capital of France? return json';
226+
227+
openAi.complete(
228+
prompt,
229+
[],
230+
200,
231+
{
232+
json_schema: {
233+
name: "capital_response",
234+
schema: {
235+
type: "object",
236+
properties: {
237+
capital: { type: "string" },
238+
},
239+
required: ["capital"],
240+
},
241+
},
242+
},
243+
).then((resp) => {
244+
console.log(resp);
245+
});
246+
247+
```
248+
249+
Then output will be like:
250+
```
251+
{ content: '{"capital":"Paris"}', finishReason: 'stop' }
252+
```
214253

215254
## 🔎 Image Analysis
216255

adminforth/documentation/docs/tutorial/08-Plugins/10-i18n.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -717,24 +717,28 @@ Response will look like this:
717717

718718
```
719719
"languages": [
720-
{
721-
"code": "en",
722-
"nameOnNative": "English",
723-
"nameEnglish": "English",
724-
"emojiFlag": "🇺🇸"
725-
},
726-
{
727-
"code": "uk",
728-
"nameOnNative": "Українська",
729-
"nameEnglish": "Ukrainian",
730-
"emojiFlag": "🇺🇦"
731-
},
732-
{
733-
"code": "ar",
734-
"nameOnNative": "العربية",
735-
"nameEnglish": "Arabic",
736-
"emojiFlag": "🇦🇷"
737-
},
720+
{
721+
"code": "en",
722+
"nameOnNative": "English",
723+
"nameEnglish": "English",
724+
"emojiFlag": "🇺🇸",
725+
"svgFlagB64": "data:image/svg+xml;base64,..."
726+
},
727+
{
728+
"code": "uk",
729+
"nameOnNative": "Українська",
730+
"nameEnglish": "Ukrainian",
731+
"emojiFlag": "🇺🇦",
732+
"svgFlagB64": "data:image/svg+xml;base64...
733+
},
734+
{
735+
"code": "ja",
736+
"nameOnNative": "日本語",
737+
"nameEnglish": "Japanese",
738+
"emojiFlag": "🇯🇵",
739+
"svgFlagB64": "data:image/svg+xml;base64..."
740+
},
741+
]
738742
```
739743

740744
### Disable translation for admin (external app only)

adminforth/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SQLiteConnector from './dataConnectors/sqlite.js';
66
import CodeInjector from './modules/codeInjector.js';
77
import ExpressServer from './servers/express.js';
88
// import FastifyServer from './servers/fastify.js';
9-
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn } from './modules/utils.js';
9+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn, convertPeriodToSeconds } from './modules/utils.js';
1010
import {
1111
type AdminForthConfig,
1212
type IAdminForth,
@@ -46,10 +46,9 @@ export * from './types/Back.js';
4646
export * from './types/Common.js';
4747
export * from './types/adapters/index.js';
4848
export * from './modules/filtersTools.js';
49-
5049
export { interpretResource };
5150
export { AdminForthPlugin };
52-
export { suggestIfTypo, RateLimiter, RAMLock, getClientIp };
51+
export { suggestIfTypo, RateLimiter, RAMLock, getClientIp, convertPeriodToSeconds };
5352

5453

5554
class AdminForth implements IAdminForth {

adminforth/modules/codeInjector.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ function hashify(obj) {
6464
return md5hash(JSON.stringify(obj));
6565
}
6666

67+
function isFulfilled<T>(result: PromiseSettledResult<T>): result is PromiseFulfilledResult<T> {
68+
return result.status === 'fulfilled';
69+
}
70+
6771
function notifyWatcherIssue(limit) {
6872
afLogger.info('Ran out of file handles after watching %s files.', limit);
6973
afLogger.info('Falling back to polling which uses more CPU.');
@@ -86,6 +90,36 @@ class CodeInjector implements ICodeInjector {
8690
return path.join(TMP_DIR, 'adminforth', brandSlug, 'spa_tmp');
8791
}
8892

93+
async checkIconNames(icons: string[]) {
94+
const uniqueIcons = Array.from(new Set(icons));
95+
const collections = new Set(icons.map((icon) => icon.split(':')[0]));
96+
const iconPackageNames = Array.from(collections).map((collection) => `@iconify-prerendered/vue-${collection}`);
97+
98+
const iconPackages = (
99+
await Promise.allSettled(iconPackageNames.map(async (pkg) => ({ pkg: await import(this.spaTmpPath() +'/node_modules/' + pkg), name: pkg})))
100+
);
101+
102+
const loadedIconPackages = iconPackages.filter(isFulfilled).map((res) => res.value).reduce((acc, { pkg, name }) => {
103+
acc[name.slice(`@iconify-prerendered/vue-`.length)] = pkg;
104+
return acc;
105+
}, {});
106+
107+
uniqueIcons.forEach((icon) => {
108+
const [ collection, iconName ] = icon.split(':');
109+
const PascalIconName = 'Icon' + iconName.split('-').map((part, index) => {
110+
return part[0].toUpperCase() + part.slice(1);
111+
}).join('');
112+
113+
if (!loadedIconPackages[collection]) {
114+
throw new Error(`Collection ${collection} not found`);
115+
}
116+
if (!loadedIconPackages[collection][PascalIconName]) {
117+
throw new Error(`Icon ${iconName} not found in collection ${collection}`);
118+
}
119+
});
120+
}
121+
122+
89123
registerCustomComponent(filePath: string): void {
90124
const componentName = getComponentNameFromPath(filePath);
91125
this.allComponentNames[filePath] = componentName;
@@ -497,7 +531,6 @@ class CodeInjector implements ICodeInjector {
497531
}
498532
}
499533

500-
501534
customResourceComponents.forEach((filePath) => {
502535
const componentName = getComponentNameFromPath(filePath);
503536
this.allComponentNames[filePath] = componentName;
@@ -528,7 +561,6 @@ class CodeInjector implements ICodeInjector {
528561
let imports = iconImports + '\n';
529562
imports += customComponentsImports + '\n';
530563

531-
532564
if (this.adminforth.config.customization?.vueUsesFile) {
533565
imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
534566
}
@@ -654,6 +686,7 @@ class CodeInjector implements ICodeInjector {
654686

655687
try {
656688
const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
689+
await this.checkIconNames(icons);
657690
if (existingHash === fullHash) {
658691
afLogger.trace(`🪲Hashes match, skipping npm ci/install, from file: ${existingHash}, actual: ${fullHash}`);
659692
return;
@@ -693,7 +726,7 @@ class CodeInjector implements ICodeInjector {
693726
}
694727
});
695728
}
696-
729+
await this.checkIconNames(icons);
697730
await fs.promises.writeFile(hashPath, fullHash);
698731
}
699732

adminforth/modules/restApi.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
733733
'edit': 'show',
734734
}[source];
735735

736-
for (const hook of listify(resource.hooks?.[hookSource]?.beforeDatasourceRequest)) {
736+
for (const hook of listify(resource.hooks?.[hookSource]?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) {
737737
const filterTools = filtersTools.get(body);
738738
body.filtersTools = filterTools;
739-
const resp = await hook({
739+
const resp = await (hook as BeforeDataSourceRequestFunction)({
740740
resource,
741741
query: body,
742742
adminUser,
@@ -1302,12 +1302,14 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
13021302
error: createRecordResponse.error,
13031303
ok: false,
13041304
newRecordId: createRecordResponse.redirectToRecordId ? createRecordResponse.redirectToRecordId :createRecordResponse.newRecordId,
1305-
redirectToRecordId: createRecordResponse.redirectToRecordId };
1305+
redirectToRecordId: createRecordResponse.redirectToRecordId
1306+
};
13061307
}
13071308
const connector = this.adminforth.connectors[resource.dataSource];
13081309

13091310
return {
13101311
newRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
1312+
redirectToRecordId: createRecordResponse.createdRecord[connector.getPrimaryKey(resource)],
13111313
ok: true
13121314
}
13131315
}

adminforth/modules/utils.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,20 @@ export function md5hash(str:string) {
381381
return crypto.createHash('md5').update(str).digest('hex');
382382
}
383383

384+
export function convertPeriodToSeconds(period: string): number {
385+
const periodChar = period.slice(-1);
386+
const duration = parseInt(period.slice(0, -1));
387+
if (periodChar === 's') {
388+
return duration;
389+
} else if (periodChar === 'm') {
390+
return duration * 60;
391+
} else if (periodChar === 'h') {
392+
return duration * 60 * 60;
393+
} else if (periodChar === 'd') {
394+
return duration * 60 * 60 * 24;
395+
}
396+
throw new Error(`Invalid period: ${period}`);
397+
}
384398
export class RateLimiter {
385399
// constructor, accepts string like 10/10m, or 20/10s, or 30/1d
386400

@@ -394,18 +408,7 @@ export class RateLimiter {
394408
}
395409

396410

397-
const period = rate.slice(-1);
398-
const duration = parseInt(rate.slice(0, -1));
399-
if (period === 's') {
400-
return duration;
401-
} else if (period === 'm') {
402-
return duration * 60;
403-
} else if (period === 'h') {
404-
return duration * 60 * 60;
405-
} else if (period === 'd') {
406-
return duration * 60 * 60 * 24;
407-
}
408-
throw new Error(`Invalid rate duration period: ${period}`);
411+
return convertPeriodToSeconds(rate);
409412
}
410413

411414

adminforth/spa/src/afcl/Table.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
</tr>
7979
</tbody>
8080
</table>
81-
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
81+
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground pt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
8282
v-if="totalPages > 1"
8383
:aria-label="$t('Table navigation')"
8484
:class="makePaginationSticky ? 'sticky bottom-0 pt-4' : ''"

adminforth/spa/src/components/GroupsTable.vue

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,23 @@
2222
class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
2323
:class="{ 'border-b': i !== group.columns.length - 1}"
2424
>
25-
<td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
25+
<td class="px-6 py-4 flex items-center block pb-0 md:pb-4 relative md:table-cell"
2626
:class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
27-
<span class="flex items-center gap-1">
28-
{{ column.label }}
29-
<Tooltip v-if="column.required[mode]">
27+
<div class="absolute inset-0 flex items-center overflow-hidden px-6 py-4 max-h-32">
28+
<span class="flex items-center gap-1">
29+
{{ column.label }}
30+
<Tooltip v-if="column.required[mode]">
3031

31-
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
32-
:class="(columnError(column) && validating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
33-
/>
32+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
33+
:class="(columnError(column) && validating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
34+
/>
3435

35-
<template #tooltip>
36-
{{ $t('Required field') }}
37-
</template>
38-
</Tooltip>
39-
</span>
36+
<template #tooltip>
37+
{{ $t('Required field') }}
38+
</template>
39+
</Tooltip>
40+
</span>
41+
</div>
4042
</td>
4143
<td
4244
class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"

0 commit comments

Comments
 (0)