Skip to content

Commit cd0dfb4

Browse files
committed
feat(strapi-admin-extensions): enable to import VAC as CSV
1 parent ae0360a commit cd0dfb4

27 files changed

+831
-2
lines changed

.changeset/rare-cows-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frameless/strapi-admin-extensions": major
3+
---
4+
5+
Maak het mogelijk om VAC als een CSV-bestand te importeren via de API.

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ module.exports = {
5353
project: [
5454
'./apps/overige-objecten-api/tsconfig.json',
5555
'./apps/overige-objecten-api/tsconfig.test.json',
56+
'./apps/strapi-admin-extensions/tsconfig.json',
57+
'./apps/strapi-admin-extensions/tsconfig.test.json',
5658
'./apps/kennisbank-dashboard/src/admin/tsconfig.json',
5759
'./apps/kennisbank-dashboard/tsconfig.json',
5860
'./apps/kennisbank-frontend/tsconfig.json',

Dockerfile.dev

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ COPY ./apps/vth-dashboard/package.json apps/vth-dashboard/package.json
1616
COPY ./apps/vth-frontend/package.json apps/vth-frontend/package.json
1717
COPY ./apps/kennisbank-dashboard/package.json apps/kennisbank-dashboard/package.json
1818
COPY ./apps/overige-objecten-api/package.json apps/overige-objecten-api/package.json
19+
COPY ./apps/strapi-admin-extensions/package.json apps/strapi-admin-extensions/package.json
1920
COPY ./apps/kennisbank-frontend/package.json apps/kennisbank-frontend/package.json
2021
COPY ./packages/catalogi-data/package.json packages/catalogi-data/package.json
2122
COPY ./packages/preview-button/package.json packages/preview-button/package.json
@@ -37,7 +38,7 @@ COPY ./packages/strapi-plugin-language/package.json packages/strapi-plugin-langu
3738
FROM build AS dependencies
3839
# Install prod dependencies
3940
COPY ./patches /opt/app/patches
40-
RUN yarn install
41+
RUN yarn install --frozen-lockfile
4142

4243
# Build target builder #
4344
########################
@@ -55,7 +56,8 @@ RUN npm run build --workspace @frameless/upl && \
5556
npm run build --workspace @frameless/strapi-plugin-uuid-field && \
5657
npm run build --workspace @frameless/strapi-plugin-env-label && \
5758
npm run build --workspace @frameless/strapi-plugin-language && \
58-
npm run build --workspace @frameless/overige-objecten-api
59+
npm run build --workspace @frameless/overige-objecten-api && \
60+
npm run build --workspace @frameless/strapi-admin-extensions
5961

6062
# Build target production #
6163
###########################
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Strapi Admin Extensions
2+
3+
This project contains custom extensions for the Strapi admin panel. It depends on another app called Strapi dashboard.
4+
5+
## Prerequisites
6+
7+
- Ensure `pdc-dashboard` is installed and set up properly before using `strapi-admin-extensions`.
8+
9+
## Installation
10+
11+
1. **Clone the repository:**
12+
13+
```bash
14+
git clone git@github.com:frameless/strapi.git
15+
```
16+
17+
2. **Install dependencies:**
18+
Make sure you are in the project root:
19+
20+
```bash
21+
yarn install
22+
```
23+
24+
## Usage
25+
26+
1. Ensure the `pdc-dashboard` app is running:
27+
28+
```bash
29+
yarn workspace @frameless/pdc-dashboard dev
30+
```
31+
32+
2. Copy the environment configuration file to the `strapi-admin-extensions` folder:
33+
34+
```bash
35+
cp .env.example .env
36+
```
37+
38+
3. Run the development server for `strapi-admin-extensions`:
39+
40+
```bash
41+
yarn workspace @frameless/strapi-admin-extensions dev
42+
```
43+
44+
## Contributing
45+
46+
We welcome contributions! Feel free to:
47+
48+
- Open an issue to report bugs or suggest new features.
49+
- Submit a pull request with improvements or fixes.
50+
51+
## License
52+
53+
This project is licensed under the EUPL-1.2 License.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
preset: 'ts-jest',
5+
// to obtain access to the matchers.
6+
moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'node'],
7+
setupFilesAfterEnv: ['<rootDir>/src/tests/jest.setup.ts'],
8+
modulePaths: ['<rootDir>'],
9+
testEnvironment: 'node',
10+
roots: ['<rootDir>/src'],
11+
transform: {
12+
'^.+\\.(ts)$': [
13+
'ts-jest',
14+
{
15+
tsconfig: 'tsconfig.test.json',
16+
},
17+
],
18+
},
19+
};
20+
21+
export default config;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@frameless/strapi-admin-extensions",
3+
"version": "0.0.0",
4+
"private": true,
5+
"author": "@frameless",
6+
"description": "Strapi Admin Extensions",
7+
"license": "EUPL-1.2",
8+
"keywords": [],
9+
"scripts": {
10+
"prebuild": "yarn clean",
11+
"build": "npm-run-all --parallel build:*",
12+
"build:server": "tsc -p ./tsconfig.json",
13+
"watch": "tsc -p ./tsconfig.json -w",
14+
"start": "NODE_ENV=production node ./dist/src/server.js",
15+
"dev": "NODE_ENV=development nodemon src/server.ts",
16+
"clean": "rimraf dist src/types tmp",
17+
"test": "OVERIGE_OBJECTEN_API_PORT=3000 jest --coverage --forceExit --verbose",
18+
"test:watch": "OVERIGE_OBJECTEN_API_PORT=3000 jest --watch"
19+
},
20+
"dependencies": {
21+
"cors": "2.8.5",
22+
"csv-parser": "3.0.0",
23+
"dompurify": "3.2.1",
24+
"dotenv": "16.4.5",
25+
"express": "4.21.0",
26+
"lodash.memoize": "4.1.2",
27+
"lodash.merge": "4.6.2",
28+
"lodash.snakecase": "4.1.1",
29+
"p-limit": "3.0.0",
30+
"morgan": "1.10.0"
31+
},
32+
"devDependencies": {
33+
"@types/cors": "2.8.17",
34+
"@types/dompurify": "3.2.0",
35+
"@types/jest": "29.5.12",
36+
"@types/lodash.memoize": "4.1.9",
37+
"@types/lodash.merge": "4.6.9",
38+
"@types/lodash.snakecase": "4.1.9",
39+
"@types/supertest": "6.0.2",
40+
"jest": "29.7.0",
41+
"jest-fetch-mock": "3.0.3",
42+
"nodemon": "3.1.7",
43+
"rimraf": "6.0.1",
44+
"supertest": "7.0.0",
45+
"ts-jest": "29.2.3",
46+
"ts-node": "10.9.2",
47+
"typescript": "5.0.4",
48+
"@types/morgan": "1.9.9"
49+
},
50+
"repository": {
51+
"type": "git+ssh",
52+
"url": "git@github.com:frameless/strapi.git",
53+
"directory": "apps/strapi-admin-extensio0s"
54+
}
55+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { NextFunction, Request, Response } from 'express';
2+
import fs from 'node:fs';
3+
import pLimit from 'p-limit';
4+
import { CREATE_VAC } from '../../queries';
5+
// import { CreateVacResponse } from '../../strapi-product-type';
6+
import { fetchData, processCsvFile } from '../../utils';
7+
8+
const limit = pLimit(5); // Limit the number of concurrent file uploads
9+
export const importController = async (req: Request, res: Response, next: NextFunction) => {
10+
const type = req.body.type;
11+
if (req.file) {
12+
const filePath = req.file.path;
13+
const requiredColumns = ['vraag', 'antwoord'];
14+
15+
try {
16+
// Process the CSV file and sanitize results
17+
const authorizationHeader = req.headers?.authorization || '';
18+
const [authType, authToken] = authorizationHeader.split(/\s+/);
19+
const tokenAuth = authType === 'Token' ? authToken : authorizationHeader;
20+
const graphqlURL = new URL('/graphql', process.env.STRAPI_PRIVATE_URL);
21+
const sanitizedResults = await processCsvFile(filePath, requiredColumns);
22+
const locale = req.query?.locale || 'nl';
23+
24+
if (type === 'vac') {
25+
// Loop through the sanitized results and create entries one by one
26+
const results = await Promise.all(
27+
sanitizedResults.map((entry) =>
28+
limit(async () => {
29+
try {
30+
const { data: responseData } = await fetchData<any>({
31+
url: graphqlURL.href,
32+
query: CREATE_VAC,
33+
variables: { locale, data: entry },
34+
headers: {
35+
Authorization: `Bearer ${tokenAuth}`,
36+
},
37+
});
38+
return responseData;
39+
} catch (error: any) {
40+
next(error);
41+
// eslint-disable-next-line no-console
42+
console.error('Error processing entry:', error);
43+
return { error: error.message, entry };
44+
}
45+
}),
46+
),
47+
);
48+
res.json({ message: 'CSV converted to JSON', data: results });
49+
// Delete temporary file after processing
50+
await fs.promises.unlink(filePath);
51+
} else {
52+
res.status(400).send('Invalid import type.');
53+
}
54+
} catch (error) {
55+
await fs.promises.unlink(filePath); // Delete the temporary file in case of error
56+
// Forward any errors to the error handler middleware
57+
next(error);
58+
return null;
59+
}
60+
} else {
61+
res.status(400).send('No file uploaded.');
62+
}
63+
return null;
64+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { importController } from './import';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const gql = (query: any) => query;
2+
3+
export const CREATE_VAC = gql(`
4+
mutation createVac($data: VacInput!) {
5+
createVac(data: $data){
6+
data {
7+
id
8+
attributes {
9+
createdAt
10+
publishedAt
11+
vac {
12+
id
13+
vraag
14+
antwoord(pagination: { start: 0, limit: -1 }) {
15+
content
16+
kennisartikelCategorie
17+
}
18+
status
19+
doelgroep
20+
uuid
21+
toelichting
22+
afdelingen {
23+
afdelingId
24+
afdelingNaam
25+
}
26+
trefwoorden {
27+
id
28+
trefwoord
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
35+
`);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import express from 'express';
2+
import multer from 'multer';
3+
import { importController } from '../../controllers';
4+
const upload = multer({ dest: 'tmp/uploads/' });
5+
const router = express.Router({ mergeParams: true });
6+
7+
router.post('/import', upload.single('file'), importController);
8+
export default router;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import importRoute from './import';
2+
export { importRoute };
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { CorsOptions } from 'cors';
2+
import cors from 'cors';
3+
import { config } from 'dotenv';
4+
import express from 'express';
5+
import { NextFunction, Request, Response } from 'express';
6+
import morgan from 'morgan';
7+
import { importRoute } from './routers';
8+
import { envAvailability, ErrorHandler } from './utils';
9+
config();
10+
11+
// Validate environment variables
12+
envAvailability({
13+
env: process.env,
14+
keys: ['STRAPI_PRIVATE_URL', 'STRAPI_ADMIN_EXTENSIONS_PORT'],
15+
});
16+
17+
const whitelist = process.env.STRAPI_ADMIN_EXTENSIONS_CORS?.split(', ') || [];
18+
const corsOption: CorsOptions = {
19+
origin: (origin, callback) => {
20+
if (!origin || whitelist.indexOf(origin) !== -1) {
21+
callback(null, true);
22+
} else {
23+
callback(
24+
new ErrorHandler('Not allowed by CORS', {
25+
statusCode: 403,
26+
}),
27+
);
28+
}
29+
},
30+
optionsSuccessStatus: 200,
31+
};
32+
const app = express();
33+
// Multer file upload middleware.
34+
// The order is important, so this should be before the express.json() middleware to parse the file.
35+
app.use('/api/v2', importRoute);
36+
// parse application/json
37+
app.use(express.json());
38+
// parse application/x-www-form-urlencoded
39+
app.use(express.urlencoded({ extended: true }));
40+
// log HTTP requests
41+
app.use(morgan('dev'));
42+
43+
const port = process.env.STRAPI_ADMIN_EXTENSIONS_PORT;
44+
// Centralized error handler middleware
45+
const globalErrorHandler = (err: ErrorHandler, _req: Request, res: Response, _next: NextFunction) => {
46+
if (err instanceof ErrorHandler || (err as ErrorHandler)?.isOperational) {
47+
// Send the proper error response with status code and message
48+
return res.status(err?.options?.statusCode || 500).json({
49+
message: err.message,
50+
});
51+
}
52+
53+
// If it's an unknown error (not an operational error), log it and send a generic response
54+
// eslint-disable-next-line no-console
55+
console.error('Unexpected error:', err);
56+
return res.status(500).json({
57+
message: 'An unexpected error occurred.',
58+
});
59+
};
60+
61+
/**
62+
* CORS
63+
* Enable CORS with a whitelist of allowed origins
64+
*/
65+
app.use(cors(corsOption));
66+
// handle non existing routes
67+
app.use((_req, res) => {
68+
res.status(404).send('Route not found');
69+
});
70+
// Use global error handler middleware
71+
app.use(globalErrorHandler);
72+
/**
73+
* Start the server
74+
*/
75+
if (process.env.NODE_ENV !== 'test') {
76+
app.listen(port, () => {
77+
// eslint-disable-next-line no-console
78+
console.log(`Overige Objecten app listening on port ${port}!`);
79+
});
80+
}
81+
82+
export default app;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import fetchMock from 'jest-fetch-mock';
2+
3+
fetchMock.enableMocks();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
interface EnvValidator {
2+
env: any;
3+
keys: string[];
4+
}
5+
export const envAvailability = ({ env, keys }: EnvValidator) => {
6+
keys?.forEach((key: string) => {
7+
if (!env[key]) {
8+
throw new Error(`Missing required environment variable: ${key}`);
9+
}
10+
});
11+
};

0 commit comments

Comments
 (0)