Skip to content

Commit fe4a281

Browse files
authored
Merge pull request #395 from devforth/next
Next
2 parents 3e95d44 + f086b33 commit fe4a281

File tree

34 files changed

+784
-115
lines changed

34 files changed

+784
-115
lines changed

adapters/install-adapters.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env bash
22
ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses \
33
adminforth-email-adapter-mailgun adminforth-google-oauth-adapter adminforth-github-oauth-adapter \
4-
adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter \
5-
adminforth-twitch-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 \
4+
adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter \
5+
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 \
77
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha"
88

adminforth/commands/createCustomComponent/configLoader.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@ import fs from 'fs/promises';
22
import path from 'path';
33
import chalk from 'chalk';
44
import jiti from 'jiti';
5-
import dotenv from "dotenv";
5+
import dotenv, { config } from "dotenv";
66

77
dotenv.config({ path: '.env.local', override: true });
88
dotenv.config({ path: '.env', override: true });
99

10-
export async function loadAdminForthConfig() {
10+
export async function getAdminInstance() {
1111
const configFileName = 'index.ts';
1212
const configPath = path.resolve(process.cwd(), configFileName);
13-
1413
try {
1514
await fs.access(configPath);
1615
} catch (error) {
1716
console.error(chalk.red(`\nError: Configuration file not found at ${configPath}`));
1817
console.error(chalk.yellow(`Please ensure you are running this command from your project's root directory and the '${configFileName}' file exists.`));
19-
process.exit(1);
18+
return null;
2019
}
2120

2221
try {
@@ -29,8 +28,19 @@ export async function loadAdminForthConfig() {
2928
const configModule = _require(configPath);
3029

3130
const adminInstance = configModule.admin || configModule.default?.admin;
31+
return { adminInstance, configPath, configFileName };
32+
} catch (error) {
33+
console.error(chalk.red(`\nError loading or parsing configuration file: ${configPath}`));
34+
console.error(error);
35+
return null;
36+
}
37+
}
3238

39+
export async function loadAdminForthConfig() {
40+
41+
const { adminInstance, configPath, configFileName } = await getAdminInstance();
3342

43+
try {
3444
if (!adminInstance) {
3545
throw new Error(`Could not find 'admin' export in ${configFileName}. Please ensure your config file exports the AdminForth instance like: 'export const admin = new AdminForth({...});'`);
3646
}
@@ -53,7 +63,7 @@ export async function loadAdminForthConfig() {
5363
return config;
5464

5565
} catch (error) {
56-
console.error(chalk.red(`\nError loading or parsing configuration file: ${configPath}`));
66+
console.error(chalk.red(`\nError loading or parsing configuration file: ${configPath}, error: ${error}`));
5767
console.error(error);
5868
process.exit(1);
5969
}

adminforth/commands/createPlugin/templates/package.json.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"types": "dist/index.d.ts",
66
"type": "module",
77
"scripts": {
8-
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch"
8+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
99
},
1010
"keywords": [],
1111
"author": "",

adminforth/commands/generateModels.js

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import fs from "fs";
22
import path from "path";
33
import { toPascalCase, mapToTypeScriptType, getInstance } from "./utils.js";
44
import dotenv from "dotenv";
5+
import { callTsProxy } from "./callTsProxy.js";
6+
import { getAdminInstance } from "../commands/createCustomComponent/configLoader.js";
57

68
const envFileArg = process.argv.find((arg) => arg.startsWith("--env-file="));
79
const envFilePath = envFileArg ? envFileArg.split("=")[1] : ".env";
@@ -23,39 +25,45 @@ async function generateModels() {
2325
}
2426

2527
let modelContent = "// Generated model file\n\n";
26-
const files = fs.readdirSync(currentDirectory);
2728
let instanceFound = false;
2829

29-
for (const file of files) {
30-
if (file.endsWith(".js") || file.endsWith(".ts")) {
31-
const instance = await getInstance(file, currentDirectory);
32-
if (instance) {
33-
await instance.discoverDatabases();
34-
instanceFound = true;
35-
instance.config.resources.forEach((resource) => {
36-
if (resource.columns) {
37-
modelContent += `export type ${toPascalCase(
38-
resource.resourceId
39-
)} = {\n`;
40-
resource.columns.forEach((column) => {
41-
if (column.name && column.type) {
42-
modelContent += ` ${column.name}: ${mapToTypeScriptType(
43-
column.type
44-
)};\n`;
30+
const { adminInstance, configPath, configFileName } = await getAdminInstance();
31+
if (adminInstance) {
32+
await adminInstance.discoverDatabases();
33+
instanceFound = true;
34+
for (const resource of adminInstance.config.resources) {
35+
if (resource.columns) {
36+
const typeName = toPascalCase(resource.resourceId);
37+
const tsCode = `
38+
export async function exec() {
39+
const columns = ${JSON.stringify(resource.columns)};
40+
const typeName = "${typeName}";
41+
function mapToTypeScriptType(type) {
42+
const map = { "integer": "number", "varchar": "string", "boolean": "boolean", "date": "string", "datetime": "string", "decimal": "number", "float": "number", "json": "Record<string, any>", "text": "string", "string": "string", "time": "string" };
43+
return map[type] || "any";
44+
}
45+
46+
let typeStr = \`export type \${typeName} = {\\n\`;
47+
for (const col of columns) {
48+
if (col.name && col.type) {
49+
typeStr += \` \${col.name}: \${mapToTypeScriptType(col.type)};\\n\`;
4550
}
46-
});
47-
modelContent += `}\n\n`;
51+
}
52+
typeStr += "}\\n\\n";
53+
return typeStr;
4854
}
49-
});
55+
`;
56+
57+
const result = await callTsProxy(tsCode);
58+
modelContent += result;
5059
}
51-
}
60+
};
5261
}
5362

5463
if (!instanceFound) {
5564
console.error("Error: No valid instance found to generate models.");
5665
return;
5766
}
58-
5967
fs.writeFileSync(modelFilePath, modelContent, "utf-8");
6068
console.log(`Generated TypeScript model file: ${modelFilePath}`);
6169
return true;

adminforth/dataConnectors/mongo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
189189
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
190190
return value === null ? null : !!value;
191191
} else if (field.type == AdminForthDataTypes.DECIMAL) {
192+
if (value === null || value === undefined) {
193+
return null;
194+
}
192195
return value?.toString();
193196
}
194197

@@ -206,6 +209,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
206209
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
207210
return value === null ? null : (value ? true : false);
208211
} else if (field.type == AdminForthDataTypes.DECIMAL) {
212+
if (value === null || value === undefined) {
213+
return null;
214+
}
209215
return Decimal128.fromString(value?.toString());
210216
}
211217
return value;

adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ This way, when admin selects, for example, "Luxury" option for "Apartment Type"
142142
143143
### Custom SQL queries with `insecureRawSQL`
144144
145-
Rarely the sec of Filters supported by AdminForth is not enough for your needs.
145+
Rarely the set of Filters supported by AdminForth is not enough for your needs.
146146
In this case you can use `insecureRawSQL` to write your own part of where clause.
147147
148-
However the vital concern that the SQL passed to DB as is, so if you substitute any user inputs it will not be escaped and can lead to SQL injection. To miticate the issue we recommend using `sqlstring` package which will escape the inputs for you.
148+
However the vital concern that the SQL passed to DB as is, so if you substitute any user inputs it will not be escaped and can lead to SQL injection. To mitigate the issue we recommend using `sqlstring` package which will escape the inputs for you.
149149
150150
```bash
151151
npm i sqlstring
@@ -163,7 +163,7 @@ import sqlstring from 'sqlstring';
163163
if (filter.field === 'some_json_b_field') {
164164
return {
165165
// check if some_json_b_field->'$.some_field' is equal to filter.value
166-
insecureRawSQL: `some_json_b_field->'$.some_field' = ${sqlstring.escape(filter.value)}`,
166+
insecureRawSQL: `some_json_b_field->>'$.some_field' = ${sqlstring.escape(filter.value)}`,
167167
}
168168
}
169169

adminforth/documentation/docs/tutorial/03-Customization/11-dataApi.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,39 @@ const users = await admin.resource('adminuser').list(
123123
);
124124
```
125125

126+
## Using a raw SQL in queries.
127+
128+
Rarely you might want to add ciondition for some exotic SQL but still want to keep the rest of API.
129+
Technically it happened that AdminForth allows you to do this also
130+
131+
```js
132+
const minUgcAge = 18;
133+
const usersWithNoUgcAccess = await admin.resource('adminuser').list(
134+
[
135+
Filters.NEQ('role', 'Admin'),
136+
{
137+
insecureRawSQL: `(user_meta->>'age') < ${sqlstring.escape(minUgcAge)}`
138+
}
139+
140+
], 15, 0, Sorts.DESC('createdAt')
141+
);
142+
143+
This will produce next SQL query:
144+
```
145+
146+
```
147+
SELECT *
148+
FROM "adminuser"
149+
WHERE "role" != 'Admin'
150+
AND (user_meta->>'age') < 18
151+
ORDER BY "createdAt" DESC
152+
LIMIT 15 OFFSET 0;
153+
154+
```
155+
156+
157+
Finds users with age less then 18 from meta field which should be a JSONB field in Postgress.
158+
126159
## Create a new item in database
127160

128161
Signature:

0 commit comments

Comments
 (0)